The new method allows processing sending errors that happen when sending from other places of plugin then from the Sending Queue worker. After three failed attempts it pauses the sending and admin user will see an notice. [MAILPOET-4736]
387 lines
12 KiB
PHP
387 lines
12 KiB
PHP
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
|
|
|
namespace MailPoet\Mailer;
|
|
|
|
use MailPoet\Settings\SettingsController;
|
|
|
|
/**
|
|
* @phpstan-type MailerLogError array{
|
|
* "error_code"?: non-empty-string,
|
|
* "error_message": string,
|
|
* "operation": string
|
|
* }
|
|
* @phpstan-type MailerLogData array{
|
|
* "sent": array<string,int>,
|
|
* "started": int,
|
|
* "status": ?string,
|
|
* "retry_attempt": ?int,
|
|
* "retry_at": ?int,
|
|
* "error": ?MailerLogError,
|
|
* "transactional_email_last_error_at": ?int,
|
|
* "transactional_email_error_count": ?int,
|
|
* }
|
|
*/
|
|
|
|
class MailerLog {
|
|
const SETTING_NAME = 'mta_log';
|
|
const STATUS_PAUSED = 'paused';
|
|
const RETRY_ATTEMPTS_LIMIT = 3;
|
|
const RETRY_INTERVAL = 120; // seconds
|
|
|
|
/**
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return MailerLogData
|
|
*/
|
|
public static function getMailerLog(array $mailerLog = null): array {
|
|
if ($mailerLog) return $mailerLog;
|
|
$settings = SettingsController::getInstance();
|
|
$mailerLog = $settings->get(self::SETTING_NAME);
|
|
if (!$mailerLog) {
|
|
$mailerLog = self::createMailerLog();
|
|
}
|
|
/**
|
|
* The old "sent" entry was just the number of emails.
|
|
* We need to update this entry to the new data structure.
|
|
*/
|
|
$mailerLog['sent'] = is_numeric($mailerLog['sent']) ? [self::sentEntriesDate(time() - 1) => $mailerLog['sent']] : (array)$mailerLog['sent'];
|
|
return $mailerLog;
|
|
}
|
|
|
|
/**
|
|
* @return MailerLogData
|
|
*/
|
|
public static function createMailerLog(): array {
|
|
$mailerLog = [
|
|
'sent' => [],
|
|
'started' => time(),
|
|
'status' => null,
|
|
'retry_attempt' => null,
|
|
'retry_at' => null,
|
|
'error' => null,
|
|
'transactional_email_last_error_at' => null,
|
|
'transactional_email_error_count' => null,
|
|
];
|
|
$settings = SettingsController::getInstance();
|
|
$settings->set(self::SETTING_NAME, $mailerLog);
|
|
return $mailerLog;
|
|
}
|
|
|
|
/**
|
|
* @return MailerLogData
|
|
*/
|
|
public static function resetMailerLog(): array {
|
|
return self::createMailerLog();
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData $mailerLog
|
|
* @return MailerLogData
|
|
*/
|
|
public static function updateMailerLog(array $mailerLog): array {
|
|
$mailerLog = self::removeOutdatedSentInformationFromMailerlog($mailerLog);
|
|
$settings = SettingsController::getInstance();
|
|
$settings->set(self::SETTING_NAME, $mailerLog);
|
|
return $mailerLog;
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return null
|
|
* @throws \Exception
|
|
*/
|
|
public static function enforceExecutionRequirements(array $mailerLog = null) {
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
if ($mailerLog['retry_attempt'] === self::RETRY_ATTEMPTS_LIMIT) {
|
|
$mailerLog = self::pauseSending($mailerLog);
|
|
}
|
|
if (self::isSendingPaused($mailerLog)) {
|
|
throw new \Exception(__('Sending has been paused.', 'mailpoet'));
|
|
}
|
|
if (self::isSendingWaitingForRetry($mailerLog)) {
|
|
throw new \Exception(__('Sending is waiting to be retried.', 'mailpoet'));
|
|
} else {
|
|
$mailerLog['retry_at'] = null;
|
|
self::updateMailerLog($mailerLog);
|
|
}
|
|
|
|
// ensure that sending frequency has not been reached
|
|
if (self::isSendingLimitReached($mailerLog)) {
|
|
throw new \Exception(__('Sending frequency limit has been reached.', 'mailpoet'));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData $mailerLog
|
|
* @return MailerLogData
|
|
*/
|
|
public static function pauseSending($mailerLog): array {
|
|
$mailerLog['status'] = self::STATUS_PAUSED;
|
|
$mailerLog['retry_attempt'] = null;
|
|
$mailerLog['retry_at'] = null;
|
|
$mailerLog['transactional_email_last_error_at'] = null;
|
|
$mailerLog['transactional_email_error_count'] = null;
|
|
return self::updateMailerLog($mailerLog);
|
|
}
|
|
|
|
/**
|
|
* @return MailerLogData
|
|
*/
|
|
public static function resumeSending(): array {
|
|
return self::resetMailerLog();
|
|
}
|
|
|
|
/**
|
|
* Process error, doesn't increase retry_attempt so it will not block sending
|
|
*
|
|
* @param string $operation
|
|
* @param string $errorMessage
|
|
* @param int $retryInterval
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public static function processNonBlockingError(string $operation, string $errorMessage, int $retryInterval = self::RETRY_INTERVAL) {
|
|
$mailerLog = self::getMailerLog();
|
|
$mailerLog['retry_at'] = time() + $retryInterval;
|
|
$mailerLog = self::setError($mailerLog, $operation, $errorMessage);
|
|
self::updateMailerLog($mailerLog);
|
|
self::enforceExecutionRequirements();
|
|
}
|
|
|
|
/**
|
|
* Process error, increase retry_attempt and block sending if it goes above RETRY_INTERVAL
|
|
*
|
|
* @param string $operation
|
|
* @param string $errorMessage
|
|
* @param string $errorCode
|
|
* @param bool $pauseSending
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public static function processError(
|
|
string $operation,
|
|
string $errorMessage,
|
|
string $errorCode = null,
|
|
bool $pauseSending = false,
|
|
int $throttledBatchSize = null
|
|
) {
|
|
$mailerLog = self::getMailerLog();
|
|
if (!isset($throttledBatchSize) || $throttledBatchSize === 1) {
|
|
$mailerLog['retry_attempt']++;
|
|
}
|
|
$mailerLog['retry_at'] = time() + self::RETRY_INTERVAL;
|
|
$mailerLog = self::setError($mailerLog, $operation, $errorMessage, $errorCode);
|
|
self::updateMailerLog($mailerLog);
|
|
if ($pauseSending) {
|
|
self::pauseSending($mailerLog);
|
|
}
|
|
self::enforceExecutionRequirements();
|
|
}
|
|
|
|
/**
|
|
* Process error, increase transactional_email_error_count and pauses sending if it reaches retry limit
|
|
* This method is meant to be used for processing errors when sending transactional emails
|
|
* like: Confirmation Email, Preview email, Stats Notification etc.
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public static function processTransactionalEmailError(
|
|
string $operation,
|
|
string $errorMessage,
|
|
?string $errorCode = null
|
|
): void {
|
|
$mailerLog = self::getMailerLog();
|
|
$lastErrorTime = $mailerLog['transactional_email_last_error_at'] ?? null;
|
|
$ignoreErrorThreshold = time() - (2 * 60); // 2 minutes ago
|
|
// We want to log the error max one time per 2 minutes
|
|
if ($lastErrorTime && $lastErrorTime > $ignoreErrorThreshold) {
|
|
return;
|
|
}
|
|
$mailerLog = self::setError($mailerLog, $operation, $errorMessage, $errorCode);
|
|
$mailerLog['transactional_email_last_error_at'] = time();
|
|
$mailerLog['transactional_email_error_count'] = ($mailerLog['transactional_email_error_count'] ?? 0) + 1;
|
|
self::updateMailerLog($mailerLog);
|
|
if ($mailerLog['transactional_email_error_count'] > self::RETRY_ATTEMPTS_LIMIT) {
|
|
self::pauseSending($mailerLog);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData $mailerLog
|
|
* @param string $operation
|
|
* @param string $errorMessage
|
|
* @param string|null $errorCode
|
|
* @return MailerLogData
|
|
*/
|
|
public static function setError(
|
|
array $mailerLog,
|
|
string $operation,
|
|
string $errorMessage,
|
|
string $errorCode = null
|
|
): array {
|
|
$mailerLog['error'] = [
|
|
'operation' => $operation,
|
|
'error_message' => $errorMessage,
|
|
];
|
|
if ($errorCode) {
|
|
$mailerLog['error']['error_code'] = $errorCode;
|
|
}
|
|
return $mailerLog;
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return MailerLogError|null
|
|
*/
|
|
public static function getError(array $mailerLog = null): ?array {
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
return isset($mailerLog['error']) ? $mailerLog['error'] : null;
|
|
}
|
|
|
|
/**
|
|
* @return MailerLogData|null
|
|
*/
|
|
public static function incrementSentCount(): ?array {
|
|
$settings = SettingsController::getInstance();
|
|
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
|
|
$mailerLog = self::getMailerLog();
|
|
|
|
// do not increment count if sending limit is reached
|
|
if (self::isSendingLimitReached($mailerLog)) {
|
|
return null;
|
|
}
|
|
// clear previous retry count, errors, etc.
|
|
if ($mailerLog['error'] !== null) {
|
|
$mailerLog = self::clearSendingErrorLog($mailerLog);
|
|
}
|
|
|
|
// do not enforce sending limit for MailPoet's sending method
|
|
if ($mailerConfig['method'] === Mailer::METHOD_MAILPOET) {
|
|
return null;
|
|
}
|
|
|
|
$time = self::sentEntriesDate();
|
|
if (!isset($mailerLog['sent'][$time])) {
|
|
$mailerLog['sent'][$time] = 0;
|
|
}
|
|
$mailerLog['sent'][$time]++;
|
|
return self::updateMailerLog($mailerLog);
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData $mailerLog
|
|
* @return MailerLogData
|
|
*/
|
|
public static function clearSendingErrorLog(array $mailerLog): array {
|
|
$mailerLog['retry_attempt'] = null;
|
|
$mailerLog['retry_at'] = null;
|
|
$mailerLog['error'] = null;
|
|
$mailerLog['transactional_email_last_error_at'] = null;
|
|
$mailerLog['transactional_email_error_count'] = null;
|
|
return self::updateMailerLog($mailerLog);
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return bool
|
|
*/
|
|
public static function isSendingLimitReached(array $mailerLog = null): bool {
|
|
$settings = SettingsController::getInstance();
|
|
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
|
|
// do not enforce sending limit for MailPoet's sending method
|
|
if ($mailerConfig['method'] === Mailer::METHOD_MAILPOET) return false;
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
|
|
if (empty($mailerConfig['frequency'])) {
|
|
$defaultSettings = $settings->getAllDefaults();
|
|
$mailerConfig['frequency'] = $defaultSettings['mta']['frequency'];
|
|
}
|
|
$frequencyInterval = (int)$mailerConfig['frequency']['interval'] * Mailer::SENDING_LIMIT_INTERVAL_MULTIPLIER;
|
|
$frequencyLimit = (int)$mailerConfig['frequency']['emails'];
|
|
$sent = self::sentSince($frequencyInterval, $mailerLog);
|
|
return $sent >= $frequencyLimit;
|
|
}
|
|
|
|
/**
|
|
* @param int|null $sinceSeconds
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return int
|
|
*/
|
|
public static function sentSince(int $sinceSeconds = null, array $mailerLog = null): int {
|
|
|
|
if ($sinceSeconds === null) {
|
|
$settings = SettingsController::getInstance();
|
|
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
|
|
if (empty($mailerConfig['frequency'])) {
|
|
$defaultSettings = $settings->getAllDefaults();
|
|
$mailerConfig['frequency'] = $defaultSettings['mta']['frequency'];
|
|
}
|
|
$sinceSeconds = (int)$mailerConfig['frequency']['interval'] * Mailer::SENDING_LIMIT_INTERVAL_MULTIPLIER;
|
|
}
|
|
$sinceDate = date('Y-m-d H:i:s', time() - $sinceSeconds);
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
|
|
return (int)array_sum(
|
|
array_filter(
|
|
(array)$mailerLog['sent'],
|
|
function($date) use ($sinceDate): bool {
|
|
return $sinceDate <= $date;
|
|
},
|
|
\ARRAY_FILTER_USE_KEY
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clears "sent" section of the mailer log from outdated entries.
|
|
*
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return MailerLogData
|
|
*/
|
|
private static function removeOutdatedSentInformationFromMailerlog(array $mailerLog = null): array {
|
|
|
|
$settings = SettingsController::getInstance();
|
|
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
|
|
$frequencyInterval = (int)$mailerConfig['frequency']['interval'] * Mailer::SENDING_LIMIT_INTERVAL_MULTIPLIER;
|
|
$sinceDate = self::sentEntriesDate(time() - $frequencyInterval);
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
|
|
$mailerLog['sent'] = array_filter(
|
|
(array)$mailerLog['sent'],
|
|
function($date) use ($sinceDate): bool {
|
|
return $sinceDate <= $date;
|
|
},
|
|
\ARRAY_FILTER_USE_KEY
|
|
);
|
|
return $mailerLog;
|
|
}
|
|
|
|
/**
|
|
* @param int|null $timestamp
|
|
* @return string
|
|
*/
|
|
private static function sentEntriesDate(int $timestamp = null): string {
|
|
|
|
return date('Y-m-d H:i:s', $timestamp ?? time());
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return bool
|
|
*/
|
|
public static function isSendingPaused(array $mailerLog = null): bool {
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
return $mailerLog['status'] === self::STATUS_PAUSED;
|
|
}
|
|
|
|
/**
|
|
* @param MailerLogData|null $mailerLog
|
|
* @return bool
|
|
*/
|
|
public static function isSendingWaitingForRetry(array $mailerLog = null): bool {
|
|
$mailerLog = self::getMailerLog($mailerLog);
|
|
$retryAt = $mailerLog['retry_at'] ?? null;
|
|
return $retryAt && (time() <= $retryAt);
|
|
}
|
|
}
|