Skip to content

Customer email confirmation rate limit #39814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 2.4-develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Magento\Customer\Api;

use Magento\Customer\Api\Data\ConfirmationLogInterface;

interface ConfirmationEmailLogManagementInterface
{
/**
* To save confirmation log
*
* @param ConfirmationLogInterface $log
* @return ConfirmationLogInterface
*/
public function save(ConfirmationLogInterface $log): ConfirmationLogInterface;

/**
* To get confirmation log by customer ID
*
* @param int $customerId
* @return ConfirmationLogInterface|null
*/
public function getByCustomerId(int $customerId): ?ConfirmationLogInterface;

/**
* To delete confirmation log by customer ID
*
* @param int $customerId
* @return void
*/
public function deleteByCustomerId(int $customerId): void;

/**
* To check if confirmation email can send
*
* @param int $customerId
* @return bool
*/
public function canSend(int $customerId): bool;

/**
* To get the configuration value for the given path
*
* @param string $path
* @return int
*/
public function getConfig(string $path): int;
}
68 changes: 68 additions & 0 deletions app/code/Magento/Customer/Api/Data/ConfirmationLogInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Magento\Customer\Api\Data;

interface ConfirmationLogInterface
{
/**
* @var string
*/
public const CUSTOMER_ID = 'customer_id';

/**
* @var string
*/
public const EMAIL_SENT_COUNTER = 'email_sent_counter';

/**
* @var string
*/
public const LAST_EMAIL_SENT_AT = 'last_email_sent_at';


/**
* To get the customer id
*
* @return int
*/
public function getCustomerId(): int;

/**
* To set the customer id
*
* @return $this
*/
public function setCustomerId(int $customerId): self;

/**
* To get the count of emails sent.
*
* @return int
*/
public function getEmailSentCounter(): int;

/**
* To set the email sent counter value.
*
* @param int $counter
* @return $this
*/
public function setEmailSentCounter(int $counter): self;

/**
* To get the timestamp of the last email sent.
*
* @return string
*/
public function getLastEmailSentAt(): string;

/**
* To set the timestamp of the last email sent.
*
* @param string $date
* @return $this
*/
public function setLastEmailSentAt(string $date): self;
}
2 changes: 2 additions & 0 deletions app/code/Magento/Customer/Controller/Account/Confirmation.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public function execute()
return $this->getRedirect('*/*/index', ['_secure' => true]);
} catch (NoSuchEntityException $e) {
$this->messageManager->addErrorMessage(__('Wrong email.'));
} catch (LocalizedException $e) {
$this->messageManager->addErrorMessage($e->getMessage());
}
}

Expand Down
18 changes: 17 additions & 1 deletion app/code/Magento/Customer/Model/AccountManagement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Magento\Customer\Api\AccountManagementInterface;
use Magento\Customer\Api\AddressRepositoryInterface;
use Magento\Customer\Api\ConfirmationEmailLogManagementInterface;
use Magento\Customer\Api\CustomerMetadataInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\AddressInterface;
Expand Down Expand Up @@ -406,6 +407,11 @@ class AccountManagement implements AccountManagementInterface
*/
private Authenticate $authenticate;

/**
* @var ConfirmationEmailLogManagementInterface
*/
private ConfirmationEmailLogManagementInterface $confirmationEmailLogManagement;

/**
* @param CustomerFactory $customerFactory
* @param ManagerInterface $eventManager
Expand Down Expand Up @@ -446,6 +452,7 @@ class AccountManagement implements AccountManagementInterface
* @param Backend|null $eavValidator
* @param CustomerLogger|null $customerLogger
* @param Authenticate|null $authenticate
* @param ConfirmationEmailLogManagementInterface|null $confirmationEmailLogManagement
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.NPathComplexity)
Expand Down Expand Up @@ -491,7 +498,8 @@ public function __construct(
?AuthenticationInterface $authentication = null,
?Backend $eavValidator = null,
?CustomerLogger $customerLogger = null,
?Authenticate $authenticate = null
?Authenticate $authenticate = null,
?ConfirmationEmailLogManagementInterface $confirmationEmailLogManagement = null
) {
$this->customerFactory = $customerFactory;
$this->eventManager = $eventManager;
Expand Down Expand Up @@ -536,6 +544,9 @@ public function __construct(
$this->eavValidator = $eavValidator ?? $objectManager->get(Backend::class);
$this->customerLogger = $customerLogger ?? $objectManager->get(CustomerLogger::class);
$this->authenticate = $authenticate ?? $objectManager->get(Authenticate::class);
$this->confirmationEmailLogManagement = $confirmationEmailLogManagement ?? $objectManager->get(
ConfirmationEmailLogManagementInterface::class
);
}

/**
Expand All @@ -547,6 +558,9 @@ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '')
if (!$customer->getConfirmation()) {
throw new InvalidTransitionException(__("Confirmation isn't needed."));
}
if (!$this->confirmationEmailLogManagement->canSend((int) $customer->getId())) {
throw new LocalizedException(__("You have reached the limit for confirmation emails."));
}

try {
$this->getEmailNotification()->newAccount(
Expand Down Expand Up @@ -621,6 +635,8 @@ private function activateCustomer($customer, $confirmationKey)
);
}

$this->confirmationEmailLogManagement->deleteByCustomerId((int) $customer->getId());

return $customer;
}

Expand Down
159 changes: 159 additions & 0 deletions app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);

namespace Magento\Customer\Model;

use Magento\Customer\Api\ConfirmationEmailLogManagementInterface;
use Magento\Customer\Api\Data\ConfirmationLogInterface;
use Magento\Customer\Model\ResourceModel\ConfirmationLog as ResourceModel;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magento\Store\Model\ScopeInterface;

class ConfirmationEmailLogManagement implements ConfirmationEmailLogManagementInterface
{
/**
* @var string
*/
public const XML_PATH_MAX_REQUEST = 'customer/create_account/max_number_confirmation_email_requests';

/**
* @var string
*/
public const XML_PATH_MIN_TIME_INTERVAL = 'customer/create_account/min_time_between_confirmation_email_requests';

/**
* @param ResourceModel $resource
* @param ConfirmationLogFactory $confirmationLogFactory
* @param ScopeConfigInterface $scopeConfig
* @param DateTime $dateTime
*/
public function __construct(
private ResourceModel $resource,
private ConfirmationLogFactory $confirmationLogFactory,
private ScopeConfigInterface $scopeConfig,
private DateTime $dateTime
) {
}

/**
* @inheritdoc
*/
public function save(ConfirmationLogInterface $log): ConfirmationLogInterface
{
$this->resource->save($log);
return $log;
}

/**
* @inheritdoc
*/
public function getByCustomerId(int $customerId): ?ConfirmationLogInterface
{
$log = $this->confirmationLogFactory->create();
$this->resource->load($log, $customerId, 'customer_id');

return $log->getId() ? $log : null;
}

/**
* @inheritdoc
*/
public function deleteByCustomerId(int $customerId): void
{
$log = $this->getByCustomerId($customerId);
if ($log) {
$this->resource->delete($log);
}
}

/**
* @inheritdoc
*/
public function canSend(int $customerId): bool
{
$existingLog = $this->getByCustomerId($customerId);

if ($existingLog) {
return $this->processExistingLog(
$existingLog,
$this->getConfig(self::XML_PATH_MAX_REQUEST),
$this->getConfig(self::XML_PATH_MIN_TIME_INTERVAL) * 60
);
}

$this->addNewLogEntry($customerId);
return true;
}

/**
* @inheritdoc
*/
public function getConfig(string $path): int
{
return (int) $this->scopeConfig->getValue($path, ScopeInterface::SCOPE_STORE);
}

/**
* Processes an existing confirmation log by updating email counters and timestamps.
* Handles scenarios where the email limit has been exceeded.
*
* @param ConfirmationLogInterface $log
* @param int $maxEmailLimit
* @param int $minTimeBetweenEmails
* @return bool
*/
private function processExistingLog(ConfirmationLogInterface $log, int $maxEmailLimit, int $minTimeBetweenEmails): bool
{
$currentEmailCount = $log->getEmailSentCounter();
$log->setEmailSentCounter($currentEmailCount + 1);

if ($currentEmailCount >= $maxEmailLimit) {
return $this->handleMaxLimitReached($log, $minTimeBetweenEmails);
}

$log->setLastEmailSentAt($this->dateTime->gmtDate());
$this->save($log);

return true;
}

/**
* Handles the condition when the maximum limit of emails has been reached.
*
* @param ConfirmationLogInterface $log
* @param int $minTimeBetweenEmails
* @return bool
*/
private function handleMaxLimitReached(ConfirmationLogInterface $log, int $minTimeBetweenEmails): bool
{
$lastEmailTimestamp = strtotime($log->getLastEmailSentAt());
$currentTimestamp = $this->dateTime->timestamp();

$timeDifference = $currentTimestamp - $lastEmailTimestamp;

if ($timeDifference >= $minTimeBetweenEmails) {
$log->setEmailSentCounter(1);
$log->setLastEmailSentAt($this->dateTime->gmtDate());
$this->save($log);
return true;
}

return false;
}

/**
* To add the confirmation log entry if no entry present for that customer
*
* @param int $customerId
* @return void
*/
private function addNewLogEntry(int $customerId): void
{
$confirmationLog = $this->confirmationLogFactory->create();
$confirmationLog->setCustomerId($customerId);
$confirmationLog->setEmailSentCounter(1);
$confirmationLog->setLastEmailSentAt($this->dateTime->gmtDate());
$this->save($confirmationLog);
}
}
Loading