diff --git a/app/code/Magento/Customer/Api/ConfirmationEmailLogManagementInterface.php b/app/code/Magento/Customer/Api/ConfirmationEmailLogManagementInterface.php
new file mode 100644
index 0000000000000..9f534ec9113c4
--- /dev/null
+++ b/app/code/Magento/Customer/Api/ConfirmationEmailLogManagementInterface.php
@@ -0,0 +1,50 @@
+getRedirect('*/*/index', ['_secure' => true]);
} catch (NoSuchEntityException $e) {
$this->messageManager->addErrorMessage(__('Wrong email.'));
+ } catch (LocalizedException $e) {
+ $this->messageManager->addErrorMessage($e->getMessage());
}
}
diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php
index 1425d29c90186..26fa8966f93c1 100644
--- a/app/code/Magento/Customer/Model/AccountManagement.php
+++ b/app/code/Magento/Customer/Model/AccountManagement.php
@@ -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;
@@ -406,6 +407,11 @@ class AccountManagement implements AccountManagementInterface
*/
private Authenticate $authenticate;
+ /**
+ * @var ConfirmationEmailLogManagementInterface
+ */
+ private ConfirmationEmailLogManagementInterface $confirmationEmailLogManagement;
+
/**
* @param CustomerFactory $customerFactory
* @param ManagerInterface $eventManager
@@ -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)
@@ -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;
@@ -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
+ );
}
/**
@@ -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(
@@ -621,6 +635,8 @@ private function activateCustomer($customer, $confirmationKey)
);
}
+ $this->confirmationEmailLogManagement->deleteByCustomerId((int) $customer->getId());
+
return $customer;
}
diff --git a/app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php b/app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php
new file mode 100644
index 0000000000000..b563eb702f63a
--- /dev/null
+++ b/app/code/Magento/Customer/Model/ConfirmationEmailLogManagement.php
@@ -0,0 +1,159 @@
+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);
+ }
+}
diff --git a/app/code/Magento/Customer/Model/ConfirmationLog.php b/app/code/Magento/Customer/Model/ConfirmationLog.php
new file mode 100644
index 0000000000000..8ce29b3fbd4be
--- /dev/null
+++ b/app/code/Magento/Customer/Model/ConfirmationLog.php
@@ -0,0 +1,69 @@
+_init(ResourceModel::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getCustomerId(): int
+ {
+ return (int) $this->getData(self::CUSTOMER_ID);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setCustomerId(int $customerId): ConfirmationLogInterface
+ {
+ return $this->setData(self::CUSTOMER_ID, $customerId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getEmailSentCounter(): int
+ {
+ return (int) $this->getData(self::EMAIL_SENT_COUNTER);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setEmailSentCounter(int $counter): ConfirmationLogInterface
+ {
+ return $this->setData(self::EMAIL_SENT_COUNTER, $counter);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getLastEmailSentAt(): string
+ {
+ return $this->getData(self::LAST_EMAIL_SENT_AT);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setLastEmailSentAt(string $date): ConfirmationLogInterface
+ {
+ return $this->setData(self::LAST_EMAIL_SENT_AT, $date);
+ }
+}
diff --git a/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog.php b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog.php
new file mode 100644
index 0000000000000..e391dae92cb04
--- /dev/null
+++ b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog.php
@@ -0,0 +1,17 @@
+_init('customer_confirmation_log', 'id');
+ }
+}
+
+
diff --git a/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog/Collection.php
new file mode 100644
index 0000000000000..26fb56ecfd0e7
--- /dev/null
+++ b/app/code/Magento/Customer/Model/ResourceModel/ConfirmationLog/Collection.php
@@ -0,0 +1,20 @@
+_init(Model::class, ResourceModel::class);
+ }
+
+}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php
index d6dd51058a63c..9458da4c2e64f 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php
@@ -32,6 +32,7 @@ class GroupTest extends TestCase
/** @var Group */
protected $groupResourceModel;
+
/** @var ResourceConnection|MockObject */
protected $resource;
diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml
index c260cb6572ac2..10feb51c7b853 100644
--- a/app/code/Magento/Customer/etc/adminhtml/system.xml
+++ b/app/code/Magento/Customer/etc/adminhtml/system.xml
@@ -115,8 +115,27 @@
Email template chosen based on theme fallback when "Default" option is selected.
Magento\Config\Model\Config\Source\Email\Template
+
+ 1
+
+
+
+
+ Limit the number of confirmation email requests within a specified time interval. Use 0 to disable.
+ required-entry validate-zero-or-greater validate-digits
+
+ 1
+
+
+
+
+ Delay in minutes between password reset requests. Use 0 to disable.
+ required-entry validate-zero-or-greater validate-digits
+
+ 1
+
-
+
@@ -124,7 +143,7 @@
]]>
Magento\Config\Model\Config\Source\Email\Template
-
+
Magento\Config\Model\Config\Source\Yesno
diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml
index 6164cb5a2c7d1..927af2ed2b865 100644
--- a/app/code/Magento/Customer/etc/config.xml
+++ b/app/code/Magento/Customer/etc/config.xml
@@ -19,6 +19,8 @@
general
customer_create_account_email_template
customer_create_account_email_no_password_template
+ 3
+ 30
customer_create_account_email_confirmation_template
customer_create_account_email_confirmed_template
0
diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml
index 0b84a3cc0c5a7..7f88602f1da21 100644
--- a/app/code/Magento/Customer/etc/db_schema.xml
+++ b/app/code/Magento/Customer/etc/db_schema.xml
@@ -561,4 +561,21 @@
+
diff --git a/app/code/Magento/Customer/etc/db_schema_whitelist.json b/app/code/Magento/Customer/etc/db_schema_whitelist.json
index 57cf2d0e87849..af76cefad77cf 100644
--- a/app/code/Magento/Customer/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Customer/etc/db_schema_whitelist.json
@@ -369,5 +369,23 @@
"constraint": {
"PRIMARY": true
}
+ },
+ "customer_confirmation_log": {
+ "id": {
+ "type": "integer",
+ "description": "Unique identifier for the confirmation log."
+ },
+ "customer_id": {
+ "type": "integer",
+ "description": "ID of the associated customer."
+ },
+ "email_sent_counter": {
+ "type": "integer",
+ "description": "Counter for the number of confirmation emails sent."
+ },
+ "last_email_sent_at": {
+ "type": "timestamp",
+ "description": "Timestamp of the last confirmation email sent."
+ }
}
}
diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml
index 00a8597d8c364..624163e1cb5be 100644
--- a/app/code/Magento/Customer/etc/di.xml
+++ b/app/code/Magento/Customer/etc/di.xml
@@ -65,6 +65,10 @@
+
+