init
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user