404 lines
13 KiB
PHP
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);
|
|
}
|
|
}
|