geno/wp-content/plugins/mailpoet/lib/Mailer/MailerLog.php
2024-02-01 11:54:18 +00:00

404 lines
13 KiB
PHP

<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
use MailPoet\Logging\LoggerFactory;
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) {
LoggerFactory::getInstance()->getLogger(LoggerFactory::TOPIC_SENDING)->error(
'Email sending was paused due an error',
[
'error_message' => $errorMessage,
'error_code' => $errorCode,
]
);
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) {
LoggerFactory::getInstance()->getLogger(LoggerFactory::TOPIC_SENDING)->error(
'Email sending was paused due a transactional email error',
[
'error_message' => $errorMessage,
'error_code' => $errorCode,
]
);
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);
}
}