Add support for checking misconfiguration of the PHP mail function

Some hosts do perform intentional misconfiguration of the mail function, causing it not to work.

When the mail function is misconfigured, we can still access and execute the mail function from within the codebase, but we get an error.

There’s no accurate way to know the PHP mail function is misconfigured. Unless we execute the function with all the proper parameters and check the exception error message against this error `Could not instantiate mail function.`

MAILPOET-4760
This commit is contained in:
Oluwaseun Olorunsola
2022-11-17 18:27:00 +01:00
committed by Aschepikov
parent 99d0eede80
commit 0bd627d3b1
5 changed files with 181 additions and 7 deletions

View File

@ -22,6 +22,7 @@ use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Statistics\StatisticsOpensRepository;
use MailPoet\Subscribers\SubscribersCountsController;
use MailPoet\Util\Notices\DisabledMailFunctionNotice;
use MailPoet\WooCommerce\TransactionalEmails;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
@ -147,6 +148,17 @@ class Settings extends APIEndpoint {
$this->messageController->updateSuccessMessages();
}
$sendingMethodSet = $settings['mta']['method'] ?? null;
if ($sendingMethodSet === 'PHPMail') {
// check for valid mail function
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, true);
} else {
// when the user switch to a new sending method
// do not display the DisabledMailFunctionNotice
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
$this->settings->set(DisabledMailFunctionNotice::OPTION_NAME, false); // do not display notice
}
// Tracking and re-engagement Emails
$meta['showNotice'] = false;
if ($oldSettings['tracking'] !== $this->settings->get('tracking')) {

View File

@ -7,6 +7,7 @@ use MailPoet\Cron\CronTrigger;
use MailPoet\InvalidStateException;
use MailPoet\Migrator\Migrator;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Notices\DisabledMailFunctionNotice;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Doctrine\DBAL\Connection;
@ -81,6 +82,8 @@ class Activator {
$localizer = new Localizer();
$localizer->forceInstallLanguagePacks($this->wp);
$this->checkForDisabledMailFunction();
}
public function deactivate() {
@ -128,6 +131,15 @@ class Activator {
}
}
private function checkForDisabledMailFunction() {
$sendingMethodSet = $this->settings->get('mta.method', false);
if ($sendingMethodSet === 'PHPMail' || is_bool($sendingMethodSet)) {
// check for valid mail function
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, true);
}
}
private function deleteAllMailPoetTablesAndData(): void {
$prefix = Env::$dbPrefix;
if (!$prefix) {

View File

@ -3,6 +3,7 @@
namespace MailPoet\Util\Notices;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Helpers;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
@ -11,7 +12,9 @@ use MailPoet\WP\Notice;
class DisabledMailFunctionNotice {
const OPTION_NAME = 'disabled-mail-function';
const OPTION_NAME = 'disabled_mail_function_check';
const QUEUE_DISABLED_MAIL_FUNCTION_CHECK = 'queue_disabled_mail_function_check';
/** @var SettingsController */
private $settings;
@ -22,18 +25,25 @@ class DisabledMailFunctionNotice {
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var MailerFactory */
private $mailerFactory;
private $isInQueueForChecking = false;
public function __construct(
WPFunctions $wp,
SettingsController $settings,
SubscribersFeature $subscribersFeature
SubscribersFeature $subscribersFeature,
MailerFactory $mailerFactory
) {
$this->settings = $settings;
$this->wp = $wp;
$this->subscribersFeature = $subscribersFeature;
$this->mailerFactory = $mailerFactory;
}
public function init($shouldDisplay): ?string {
$shouldDisplay = $shouldDisplay && $this->checkRequirements();
$shouldDisplay = $shouldDisplay && $this->checkMisConfiguredFunction() && $this->checkRequirements();
if (!$shouldDisplay) {
return null;
}
@ -42,13 +52,43 @@ class DisabledMailFunctionNotice {
}
private function checkRequirements(): bool {
if ($this->isInQueueForChecking) {
$this->settings->set(self::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
}
$sendingMethod = $this->settings->get('mta.method', false);
$isPhpMailSendingMethod = $sendingMethod === Mailer::METHOD_PHPMAIL;
if (!$isPhpMailSendingMethod) {
return false; // fails requirements check
}
$functionName = 'mail';
$isMailFunctionDisabled = $this->isFunctionDisabled($functionName);
return $isPhpMailSendingMethod && $isMailFunctionDisabled;
if ($isMailFunctionDisabled) {
$this->settings->set(DisabledMailFunctionNotice::OPTION_NAME, true);
return true;
}
$isMailFunctionProperlyConfigured = $this->testMailFunctionIsCorrectlyConfigured();
return !$isMailFunctionProperlyConfigured;
}
/*
* Check MisConfigured Function
*
* This method will cause this class to only display the notice if the settings option
*
* disabled_mail_function_check === true
* or
* queue_disabled_mail_function_check === true
*
*/
public function checkMisConfiguredFunction(): bool {
$this->isInQueueForChecking = $this->settings->get(self::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
return $this->settings->get(self::OPTION_NAME, false) || $this->isInQueueForChecking;
}
public function isFunctionDisabled(string $function): bool {
@ -90,4 +130,87 @@ class DisabledMailFunctionNotice {
$link = $this->wp->escAttr($buttonLink);
return '<p><a target="_blank" href="' . $link . '" class="button button-primary">' . __('Connect MailPoet', 'mailpoet') . '</a></p>';
}
/*
* Test Mail Function Is Correctly Configured
*
* This is a workaround for detecting the user PHP mail() function is Correctly Configured and not disabled by the host
*/
private function testMailFunctionIsCorrectlyConfigured(): bool {
if ($this->settings->get(DisabledMailFunctionNotice::OPTION_NAME, false)) {
return false; // skip sending mail again
}
$mailBody = 'Yup, it works! You can start blasting away emails to the moon.';
$sendTestMailData = [
'mailer' => $this->settings->get('mta'),
'newsletter' => [
'subject' => 'This is a Sending Method Test',
'body' => [
'html' => "<p>$mailBody</p>",
'text' => $mailBody,
],
],
'subscriber' => 'blackhole@mailpoet.com',
];
$sendMailResult = $this->sendTestMail($sendTestMailData);
if (!$sendMailResult) {
// Error with PHP mail() function
// keep displaying notice
$this->settings->set(DisabledMailFunctionNotice::OPTION_NAME, true);
}
return $sendMailResult;
}
/*
* Send Test Mail
* used to check for valid PHP mail()
*
* returns true if valid and okay
* else returns false if invalid.
*
* We determine the mail function is invalid by checking against the Exception error thrown by PHPMailer
* error message: Could not instantiate mail function.
*
* if the error is not equal to error message, we consider it okay.
*/
public function sendTestMail($data = []): bool {
try {
$mailer = $this->mailerFactory->buildMailer(
$data['mailer'] ?? null,
$data['sender'] ?? null,
$data['reply_to'] ?? null
);
// report this as 'sending_test' in metadata since this endpoint is only used to test sending methods for now
$extraParams = [
'meta' => [
'email_type' => 'sending_test',
'subscriber_status' => 'unknown',
'subscriber_source' => 'administrator',
],
];
$result = $mailer->send($data['newsletter'], $data['subscriber'], $extraParams);
if ($result['response'] === false) {
$errorMessage = $result['error']->getMessage();
return !$this->checkForErrorMessage($errorMessage);
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
return !$this->checkForErrorMessage($errorMessage);
}
return true;
}
private function checkForErrorMessage($errorMessage): bool {
$phpmailerError = 'Could not instantiate mail function';
$substringIndex = stripos($errorMessage, $phpmailerError);
return $substringIndex !== false;
}
}

View File

@ -3,6 +3,7 @@
namespace MailPoet\Util\Notices;
use MailPoet\Config\Menu;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Subscribers\SubscribersRepository;
@ -52,7 +53,8 @@ class PermanentNotices {
TrackingConfig $trackingConfig,
SubscribersRepository $subscribersRepository,
SettingsController $settings,
SubscribersFeature $subscribersFeature
SubscribersFeature $subscribersFeature,
MailerFactory $mailerFactory
) {
$this->wp = $wp;
$this->phpVersionWarnings = new PHPVersionWarnings();
@ -65,7 +67,7 @@ class PermanentNotices {
$this->emailWithInvalidListNotice = new EmailWithInvalidSegmentNotice($wp);
$this->changedTrackingNotice = new ChangedTrackingNotice($wp);
$this->deprecatedFilterNotice = new DeprecatedFilterNotice($wp);
$this->disabledMailFunctionNotice = new DisabledMailFunctionNotice($wp, $settings, $subscribersFeature);
$this->disabledMailFunctionNotice = new DisabledMailFunctionNotice($wp, $settings, $subscribersFeature, $mailerFactory);
}
public function init() {

View File

@ -4,6 +4,7 @@ namespace MailPoet\Util\Notices;
use Codeception\Util\Stub;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\WP\Functions as WPFunctions;
@ -21,6 +22,7 @@ class DisabledMailFunctionNoticeTest extends \MailPoetTest
$this->settings = SettingsController::getInstance();
$this->wp = new WPFunctions;
$this->settings->set('mta.method', Mailer::METHOD_PHPMAIL);
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, true);
$this->wp->setTransient(SubscribersFeature::SUBSCRIBERS_COUNT_CACHE_KEY, 50, SubscribersFeature::SUBSCRIBERS_COUNT_CACHE_EXPIRATION_MINUTES * 60);
}
@ -35,9 +37,13 @@ class DisabledMailFunctionNoticeTest extends \MailPoetTest
}
private function generateNotice($methodOverride = []) {
$mailerFactoryMock = $this->createMock(MailerFactory::class);
$mailerFactoryMock->method('buildMailer')
->willReturn($this->createMock(Mailer::class));
return Stub::construct(
DisabledMailFunctionNotice::class,
[$this->wp, $this->settings, $this->diContainer->get(SubscribersFeature::class)],
[$this->wp, $this->settings, $this->diContainer->get(SubscribersFeature::class), $mailerFactoryMock],
$methodOverride
);
}
@ -81,4 +87,23 @@ class DisabledMailFunctionNoticeTest extends \MailPoetTest
expect($result)->equals(false);
}
public function testItResetQueueCheck() {
$this->settings->set(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, true);
$disabledMailFunctionNotice = $this->generateNotice();
$notice = $disabledMailFunctionNotice->init(true);
expect($notice)->equals(null);
$status = $this->settings->get(DisabledMailFunctionNotice::QUEUE_DISABLED_MAIL_FUNCTION_CHECK, false);
expect($status)->equals(false);
}
public function testItDisplayNoticeWhenMailIsMisConfigured() {
$disabledMailFunctionNotice = $this->generateNotice(['sendTestMail' => false]);
$notice = $disabledMailFunctionNotice->init(true);
expect($notice)->stringContainsString('Get ready to send your first campaign');
$status = $this->settings->get(DisabledMailFunctionNotice::OPTION_NAME, false);
expect($status)->equals(true);
}
}