From d26b1ca5465485aff4316a4a34c157c251a44f3c Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 9 Jan 2025 09:54:18 +0530 Subject: [PATCH 01/74] check queue --- lib/Service/BlackListService.php | 62 +++++++++++++++++++++++++++- lib/Service/RecoveryEmailService.php | 60 +++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php index 020c58f..095f41b 100644 --- a/lib/Service/BlackListService.php +++ b/lib/Service/BlackListService.php @@ -17,7 +17,7 @@ class BlackListService { private $appName; private const BLACKLISTED_DOMAINS_FILE_NAME = 'blacklisted_domains.json'; private const BLACKLISTED_DOMAINS_URL = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'; - + private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; public function __construct(string $appName, ILogger $logger, IFactory $l10nFactory, IAppData $appData) { $this->logger = $logger; @@ -120,4 +120,64 @@ class BlackListService { } return true; } + + /** + * Check if an domain is popular domain a JSON list of popular domains. + * + * @param string $email The email address to check. + * @return bool True if the email domain is popular, false otherwise. + */ + public function isPopularDomain(string $email): bool { + if (!$this->ensureDocumentsFolder()) { + return false; + } + $popularlistedDomains = $this->getPopularlistedDomainData(); + if (empty($popularlistedDomains)) { + return false; + } + $emailParts = explode('@', $email); + $emailDomain = strtolower(end($emailParts)); + return in_array($emailDomain, $popularlistedDomains); + } + + /** + * Retrieve the Popular domain file path + * + * @return ISimpleFile + */ + private function getPopularDomainsFile(): ISimpleFile { + try { + $currentFolder = $this->appData->getFolder('/'); + } catch (NotFoundException $e) { + $currentFolder = $this->appData->newFolder('/'); + } + $filename = self::POPULAR_DOMAINS_FILE_NAME; + if ($currentFolder->fileExists($filename)) { + return $currentFolder->getFile($filename); + } + return $currentFolder->newFile($filename); + } + + /** + * Retrieve the Popular domain data. + * + * @return array The array of popular domains. + */ + public function getPopularlistedDomainData(): array { + $document = self::POPULAR_DOMAINS_FILE_NAME; + $file = $this->getPopularDomainsFile(); + try { + $popularlistedDomainsInJson = $file->getContent(); + if (empty($popularlistedDomainsInJson)) { + return []; + } + return json_decode($popularlistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); + } catch (NotFoundException $e) { + $this->logger->warning('Popular domains file ' . $document . ' not found!'); + return []; + } catch (\Throwable $e) { + $this->logger->warning('Error decoding Popular domains file ' . $document . ': ' . $e->getMessage()); + return []; + } + } } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 0fe1844..910a186 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -39,6 +39,7 @@ class RecoveryEmailService { private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; + private const API_KEY = "c6f632a2c9bf4b718e81a3a90c27af2f"; private BlackListService $blackListService; private IL10N $l; private ISession $session; @@ -135,6 +136,39 @@ class RecoveryEmailService { $this->logger->info("User ID $username's requested recovery email address domain is blacklisted. Please provide another recovery address."); throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } + $apiKey = self::API_KEY; + if ($this->blackListService->isPopularDomain($recoveryEmail)) { + // Ensure rate limit for email verification (2 requests per second) + $this->ensureRealTimeRateLimit('rate_limit_email', 2); + + $url = "https://verifymail.io/api/$recoveryEmail?key=$apiKey"; + + $response = file_get_contents($url); + $data = json_decode($response, true); + + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is disposable"); + } else { + $this->logger->info("User ID $username's requested recovery email address is valid and not disposable"); + } + } else { + // Extract the domain from the email address + $domain = substr(strrchr($recoveryEmail, "@"), 1); + + // Ensure rate limit for domain verification (15 requests per second) + $this->ensureRealTimeRateLimit('rate_limit_domain', 15); + + // Check if the domain is disposable + $url = "https://verifymail.io/api/$domain?key=$apiKey"; + $response = file_get_contents($url); + $data = json_decode($response, true); + $this->logger->info("User ID $username's requested recovery email address domain is popular domain"); + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is from a disposable domain"); + } else { + $this->logger->info("User ID $username's requested recovery email address domain is popular but not disposable"); + } + } } return true; } @@ -307,4 +341,30 @@ class RecoveryEmailService { public function isValidEmailFormat(string $recoveryEmail): bool { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } + + private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 5): void { + $redis = \OC::$server->getMemCacheFactory()->create('redis'); + $now = microtime(true); + + for ($retry = 0; $retry < $maxRetries; $retry++) { + $requests = $redis->get($key) ?? []; + + // Remove old requests (keep only those within a 1-second window) + $requests = array_filter($requests, function ($timestamp) use ($now) { + return ($now - $timestamp) < 1; + }); + + if (count($requests) < $rateLimit) { + // Under the rate limit, add the current request timestamp and proceed + $requests[] = $now; + $redis->set($key, $requests, 2); // Set expiry of 2 seconds for cached timestamps + return; + } + + // If rate limit exceeded, wait a short time and retry + usleep(100000); // Wait 100ms before retrying (adjust as needed) + } + + throw new \Exception("Rate limit exceeded. Please try again later."); + } } -- GitLab From 3b133f4531088806667e621c6a4ae1d60f59595d Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 9 Jan 2025 12:02:10 +0530 Subject: [PATCH 02/74] fix redis cache --- lib/Service/RecoveryEmailService.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 910a186..e6c420b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -24,6 +24,7 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Util; +use OCP\ICache; class RecoveryEmailService { private ILogger $logger; @@ -44,7 +45,7 @@ class RecoveryEmailService { private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l, ICache $redisCache) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -58,6 +59,7 @@ class RecoveryEmailService { $this->curl = $curlService; $this->blackListService = $blackListService; $this->l = $l; + $this->redisCache = $redisCache; $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { @@ -343,11 +345,10 @@ class RecoveryEmailService { } private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 5): void { - $redis = \OC::$server->getMemCacheFactory()->create('redis'); $now = microtime(true); for ($retry = 0; $retry < $maxRetries; $retry++) { - $requests = $redis->get($key) ?? []; + $requests = $this->redisCache->get($key) ?? []; // Remove old requests (keep only those within a 1-second window) $requests = array_filter($requests, function ($timestamp) use ($now) { @@ -357,7 +358,7 @@ class RecoveryEmailService { if (count($requests) < $rateLimit) { // Under the rate limit, add the current request timestamp and proceed $requests[] = $now; - $redis->set($key, $requests, 2); // Set expiry of 2 seconds for cached timestamps + $this->redisCache->set($key, $requests, 2); // Set expiry of 2 seconds for cached timestamps return; } -- GitLab From 964089d424aa1cfeabd6bd7703e82e9d480af674 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 9 Jan 2025 14:46:57 +0530 Subject: [PATCH 03/74] redis cache fix --- lib/Service/RecoveryEmailService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index e6c420b..093880f 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -24,7 +24,6 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Util; -use OCP\ICache; class RecoveryEmailService { private ILogger $logger; @@ -45,7 +44,7 @@ class RecoveryEmailService { private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l, ICache $redisCache) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -59,7 +58,8 @@ class RecoveryEmailService { $this->curl = $curlService; $this->blackListService = $blackListService; $this->l = $l; - $this->redisCache = $redisCache; + // Initialize Redis cache directly + $this->redisCache = \OC::$server->getMemCacheFactory()->createDistributed(); $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { -- GitLab From 9ad8f9733ae4346357ba022667556732826fc5a5 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 10 Jan 2025 12:26:20 +0530 Subject: [PATCH 04/74] check popular domain and email --- lib/Service/RecoveryEmailService.php | 46 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 093880f..7abd758 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -139,37 +139,45 @@ class RecoveryEmailService { throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } $apiKey = self::API_KEY; + if ($this->blackListService->isPopularDomain($recoveryEmail)) { // Ensure rate limit for email verification (2 requests per second) $this->ensureRealTimeRateLimit('rate_limit_email', 2); + // Step 3.1: Verify the email address using verifymail.io $url = "https://verifymail.io/api/$recoveryEmail?key=$apiKey"; - $response = file_get_contents($url); $data = json_decode($response, true); if ($data['disposable'] ?? false) { - $this->logger->info("User ID $username's requested recovery email address is disposable"); + $this->logger->info("User ID $username's requested recovery email address is disposable."); + throw new BlacklistedEmailException("The email address is disposable. Please provide another recovery address."); + } elseif (!$data['deliverable_email'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address is not deliverable."); + throw new BlacklistedEmailException("The email address is not deliverable. Please provide another recovery address."); } else { - $this->logger->info("User ID $username's requested recovery email address is valid and not disposable"); + $this->logger->info("User ID $username's requested recovery email address is valid and not disposable."); + return true; // Email is valid } - } else { - // Extract the domain from the email address - $domain = substr(strrchr($recoveryEmail, "@"), 1); + } + + $this->ensureRealTimeRateLimit('rate_limit_domain', 15); - // Ensure rate limit for domain verification (15 requests per second) - $this->ensureRealTimeRateLimit('rate_limit_domain', 15); - - // Check if the domain is disposable - $url = "https://verifymail.io/api/$domain?key=$apiKey"; - $response = file_get_contents($url); - $data = json_decode($response, true); - $this->logger->info("User ID $username's requested recovery email address domain is popular domain"); - if ($data['disposable'] ?? false) { - $this->logger->info("User ID $username's requested recovery email address is from a disposable domain"); - } else { - $this->logger->info("User ID $username's requested recovery email address domain is popular but not disposable"); - } + $url = "https://verifymail.io/api/$domain?key=$apiKey"; + $response = file_get_contents($url); + $data = json_decode($response, true); + + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); + // Store domain and related domains in custom_disposable_domains.json + $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); + } elseif (!$data['mx'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address domain is not valid."); + throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); + } else { + $this->logger->info("User ID $username's requested recovery email address domain is valid."); + return true; // Domain and email are valid } } return true; -- GitLab From 8cae6ab665d0ddbce2954e3371ebb244d01d90b3 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 10 Jan 2025 12:50:03 +0530 Subject: [PATCH 05/74] added domain --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 7abd758..f72bddc 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -162,7 +162,7 @@ class RecoveryEmailService { } $this->ensureRealTimeRateLimit('rate_limit_domain', 15); - + $domain = substr(strrchr($recoveryEmail, "@"), 1); $url = "https://verifymail.io/api/$domain?key=$apiKey"; $response = file_get_contents($url); $data = json_decode($response, true); -- GitLab From 6fa19582b48c3fc1d671510002a8124a91c72ed4 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 10 Jan 2025 14:30:11 +0530 Subject: [PATCH 06/74] remove --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index f72bddc..32aadde 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -39,7 +39,7 @@ class RecoveryEmailService { private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; - private const API_KEY = "c6f632a2c9bf4b718e81a3a90c27af2f"; + private BlackListService $blackListService; private IL10N $l; private ISession $session; -- GitLab From 311e0b0bf7e4ee9d0d38723e09f07c4d9f632802 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 10 Jan 2025 14:35:03 +0530 Subject: [PATCH 07/74] added environment --- lib/Service/RecoveryEmailService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 32aadde..b7e6e4f 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -39,7 +39,6 @@ class RecoveryEmailService { private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; - private BlackListService $blackListService; private IL10N $l; private ISession $session; @@ -138,7 +137,7 @@ class RecoveryEmailService { $this->logger->info("User ID $username's requested recovery email address domain is blacklisted. Please provide another recovery address."); throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } - $apiKey = self::API_KEY; + $apiKey = getenv('VERIFYMAIL_API_KEY'); if ($this->blackListService->isPopularDomain($recoveryEmail)) { // Ensure rate limit for email verification (2 requests per second) -- GitLab From 809c42470806d859c8538889d94b222a761a25ce Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 10 Jan 2025 17:30:17 +0530 Subject: [PATCH 08/74] occ to add popular domain --- lib/Command/CreatePopularDomain.php | 57 ++++++++++++++++++++++++++++ lib/Service/PopularDomainService.php | 0 2 files changed, 57 insertions(+) create mode 100644 lib/Command/CreatePopularDomain.php create mode 100644 lib/Service/PopularDomainService.php diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php new file mode 100644 index 0000000..96d6d51 --- /dev/null +++ b/lib/Command/CreatePopularDomain.php @@ -0,0 +1,57 @@ +domainService = $domainService; + $this->logger = $logger; + } + + protected function configure() { + $this + ->setName('emailrecovery:fetch-popular-domains') + ->setDescription('Fetch popular email domains and create a popular_domain.json file'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $domains = $this->domainService->fetchPopularDomains(); + $file = $this->getPopularDomainsFile(); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $output->writeln('Popular domains list created successfully at ' . $filePath); + } catch (\Throwable $th) { + $this->logger->error('Error while fetching popular domains. ' . $th->getMessage()); + $output->writeln('Error while fetching popular domains: ' . $th->getMessage()); + return Command::FAILURE; + } + return Command::SUCCESS; + } + + private function getPopularDomainsFile(): \OCP\Files\ISimpleFile { + try { + $currentFolder = $this->appData->getFolder('/'); + } catch (NotFoundException $e) { + $currentFolder = $this->appData->newFolder('/'); + } + + $filename = self::POPULAR_DOMAINS_FILE_NAME; + if ($currentFolder->fileExists($filename)) { + return $currentFolder->getFile($filename); + } + + return $currentFolder->newFile($filename); + } +} diff --git a/lib/Service/PopularDomainService.php b/lib/Service/PopularDomainService.php new file mode 100644 index 0000000..e69de29 -- GitLab From 5ae0a9bc19f6becc804487f916d44388c7dde1ad Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 11:37:09 +0530 Subject: [PATCH 09/74] fix lint --- lib/Command/CreatePopularDomain.php | 86 ++++++++++++++--------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 96d6d51..23f7881 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -11,47 +11,47 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class FetchPopularDomains extends Command { - private PopularDomainService $domainService; - private ILogger $logger; - - public function __construct(PopularDomainService $domainService, ILogger $logger) { - parent::__construct(); - $this->domainService = $domainService; - $this->logger = $logger; - } - - protected function configure() { - $this - ->setName('emailrecovery:fetch-popular-domains') - ->setDescription('Fetch popular email domains and create a popular_domain.json file'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - try { - $domains = $this->domainService->fetchPopularDomains(); - $file = $this->getPopularDomainsFile(); - $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - $output->writeln('Popular domains list created successfully at ' . $filePath); - } catch (\Throwable $th) { - $this->logger->error('Error while fetching popular domains. ' . $th->getMessage()); - $output->writeln('Error while fetching popular domains: ' . $th->getMessage()); - return Command::FAILURE; - } - return Command::SUCCESS; - } - - private function getPopularDomainsFile(): \OCP\Files\ISimpleFile { - try { - $currentFolder = $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $currentFolder = $this->appData->newFolder('/'); - } - - $filename = self::POPULAR_DOMAINS_FILE_NAME; - if ($currentFolder->fileExists($filename)) { - return $currentFolder->getFile($filename); - } - - return $currentFolder->newFile($filename); - } + private PopularDomainService $domainService; + private ILogger $logger; + + public function __construct(PopularDomainService $domainService, ILogger $logger) { + parent::__construct(); + $this->domainService = $domainService; + $this->logger = $logger; + } + + protected function configure() { + $this + ->setName('emailrecovery:fetch-popular-domains') + ->setDescription('Fetch popular email domains and create a popular_domain.json file'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $domains = $this->domainService->fetchPopularDomains(); + $file = $this->getPopularDomainsFile(); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $output->writeln('Popular domains list created successfully at ' . $filePath); + } catch (\Throwable $th) { + $this->logger->error('Error while fetching popular domains. ' . $th->getMessage()); + $output->writeln('Error while fetching popular domains: ' . $th->getMessage()); + return Command::FAILURE; + } + return Command::SUCCESS; + } + + private function getPopularDomainsFile(): \OCP\Files\ISimpleFile { + try { + $currentFolder = $this->appData->getFolder('/'); + } catch (NotFoundException $e) { + $currentFolder = $this->appData->newFolder('/'); + } + + $filename = self::POPULAR_DOMAINS_FILE_NAME; + if ($currentFolder->fileExists($filename)) { + return $currentFolder->getFile($filename); + } + + return $currentFolder->newFile($filename); + } } -- GitLab From 7516cd3176e5ad94cd0bc3295e8eab4c7d850eee Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 11:55:30 +0530 Subject: [PATCH 10/74] addec check for disposable domain --- lib/Service/BlackListService.php | 51 +++++++++++++++++++++++++--- lib/Service/RecoveryEmailService.php | 13 ++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php index 095f41b..aaf463d 100644 --- a/lib/Service/BlackListService.php +++ b/lib/Service/BlackListService.php @@ -18,7 +18,7 @@ class BlackListService { private const BLACKLISTED_DOMAINS_FILE_NAME = 'blacklisted_domains.json'; private const BLACKLISTED_DOMAINS_URL = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'; private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; - + private const DISPOSABLE_DOMAINS_FILE_NAME = 'disposable-domains.json'; public function __construct(string $appName, ILogger $logger, IFactory $l10nFactory, IAppData $appData) { $this->logger = $logger; $this->l10nFactory = $l10nFactory; @@ -141,17 +141,16 @@ class BlackListService { } /** - * Retrieve the Popular domain file path + * Retrieve the domain file * * @return ISimpleFile */ - private function getPopularDomainsFile(): ISimpleFile { + private function getDomainsFile($filename): ISimpleFile { try { $currentFolder = $this->appData->getFolder('/'); } catch (NotFoundException $e) { $currentFolder = $this->appData->newFolder('/'); } - $filename = self::POPULAR_DOMAINS_FILE_NAME; if ($currentFolder->fileExists($filename)) { return $currentFolder->getFile($filename); } @@ -165,7 +164,7 @@ class BlackListService { */ public function getPopularlistedDomainData(): array { $document = self::POPULAR_DOMAINS_FILE_NAME; - $file = $this->getPopularDomainsFile(); + $file = $this->getDomainsFile($document); try { $popularlistedDomainsInJson = $file->getContent(); if (empty($popularlistedDomainsInJson)) { @@ -180,4 +179,46 @@ class BlackListService { return []; } } + + /** + * Retrieve the Disposable domain data. + * + * @return array The array of disposable domains. + */ + public function getDisposablelistedDomainData(): array { + $document = self::DISPOSABLE_DOMAINS_FILE_NAME; + $file = $this->getDomainsFile($document); + try { + $disposablelistedDomainsInJson = $file->getContent(); + if (empty($disposablelistedDomainsInJson)) { + return []; + } + return json_decode($disposablelistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); + } catch (NotFoundException $e) { + $this->logger->warning('Disposable domains file ' . $document . ' not found!'); + return []; + } catch (\Throwable $e) { + $this->logger->warning('Error decoding Disposable domains file ' . $document . ': ' . $e->getMessage()); + return []; + } + } + + /** + * Check if an domain is custom disposable domain a JSON list of disposable domains. + * + * @param string $email The email address to check. + * @return bool True if the email domain is custom disposable domain, false otherwise. + */ + public function isDomainInCustomDisposable(string $email): bool { + if (!$this->ensureDocumentsFolder()) { + return false; + } + $customlistedDomains = $this->getDisposablelistedDomainData(); + if (empty($customlistedDomains)) { + return false; + } + $emailParts = explode('@', $email); + $emailDomain = strtolower(end($emailParts)); + return in_array($emailDomain, $customlistedDomains); + } } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index b7e6e4f..ed056fd 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -139,7 +139,7 @@ class RecoveryEmailService { } $apiKey = getenv('VERIFYMAIL_API_KEY'); - if ($this->blackListService->isPopularDomain($recoveryEmail)) { + if ($this->blackListService->isPopularDomain($recoveryEmail) && $this->blackListService->isDomainInCustomDisposable($recoveryEmail)) { // Ensure rate limit for email verification (2 requests per second) $this->ensureRealTimeRateLimit('rate_limit_email', 2); @@ -375,4 +375,15 @@ class RecoveryEmailService { throw new \Exception("Rate limit exceeded. Please try again later."); } + + private function isDomainInCustomBlacklist(string $domain): bool { + $filePath = __DIR__ . '/../data/custom_disposable_domains.json'; + + if (!file_exists($filePath)) { + return false; // If file doesn't exist, the domain is not blacklisted + } + + $blacklistedDomains = json_decode(file_get_contents($filePath), true); + return in_array($domain, $blacklistedDomains, true); + } } -- GitLab From b77169ce8e81783a3b0f9ce72b090b1fa9714936 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 12:22:38 +0530 Subject: [PATCH 11/74] fix app id --- lib/Command/CreatePopularDomain.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 23f7881..e64fb1f 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -22,7 +22,7 @@ class FetchPopularDomains extends Command { protected function configure() { $this - ->setName('emailrecovery:fetch-popular-domains') + ->setName(Application::APP_ID.':fetch-popular-domains') ->setDescription('Fetch popular email domains and create a popular_domain.json file'); } -- GitLab From 5b3d8106cb5af1a5ca5d5c162d415127d475e9a8 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 12:25:34 +0530 Subject: [PATCH 12/74] fix app id --- appinfo/info.xml | 1 + lib/Command/CreatePopularDomain.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 90788aa..c2004f4 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,5 +22,6 @@ OCA\EmailRecovery\Command\UpdateBlacklistedDomains + OCA\EmailRecovery\Command\CreatePopularDomain diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index e64fb1f..9886ba6 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -10,7 +10,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class FetchPopularDomains extends Command { +class CreatePopularDomain extends Command { private PopularDomainService $domainService; private ILogger $logger; -- GitLab From 6accd2e08f84e220099235eeacfb234a2660bdef Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 12:33:17 +0530 Subject: [PATCH 13/74] popular domain service add --- lib/Command/CreatePopularDomain.php | 3 +- lib/Service/PopularDomainService.php | 47 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 9886ba6..63c900e 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace OCA\EmailRecovery\Command; +use OCA\EmailRecovery\AppInfo\Application; use OCA\EmailRecovery\Service\PopularDomainService; use OCP\ILogger; use Symfony\Component\Console\Command\Command; @@ -22,7 +23,7 @@ class CreatePopularDomain extends Command { protected function configure() { $this - ->setName(Application::APP_ID.':fetch-popular-domains') + ->setName(Application::APP_ID.':create-popular-domains') ->setDescription('Fetch popular email domains and create a popular_domain.json file'); } diff --git a/lib/Service/PopularDomainService.php b/lib/Service/PopularDomainService.php index e69de29..a4d9a66 100644 --- a/lib/Service/PopularDomainService.php +++ b/lib/Service/PopularDomainService.php @@ -0,0 +1,47 @@ +httpClient = $httpClient; + $this->logger = $logger; + } + + /** + * Fetch popular email domains from an external source. + * + * @return array List of popular domains. + */ + public function fetchPopularDomains(): array { + $url = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/popular_domains.json'; + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); + } + + return $data; + } catch (RequestException $e) { + $this->logger->error('HTTP request failed: ' . $e->getMessage()); + throw new \RuntimeException('Failed to fetch popular domains.'); + } catch (\Throwable $th) { + $this->logger->error('Unexpected error: ' . $th->getMessage()); + throw $th; + } + } +} -- GitLab From 26d1e95b5e70bd30e20aac942a9831b409168e5e Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 12:38:34 +0530 Subject: [PATCH 14/74] popular domain service add --- lib/Service/PopularDomainService.php | 62 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/Service/PopularDomainService.php b/lib/Service/PopularDomainService.php index a4d9a66..417ad2c 100644 --- a/lib/Service/PopularDomainService.php +++ b/lib/Service/PopularDomainService.php @@ -9,39 +9,39 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; class PopularDomainService { - private Client $httpClient; - private ILogger $logger; + private Client $httpClient; + private ILogger $logger; private const POPULAR_DOMAINS_URL = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/domains.json'; private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; - public function __construct(Client $httpClient, ILogger $logger) { - $this->httpClient = $httpClient; - $this->logger = $logger; - } - - /** - * Fetch popular email domains from an external source. - * - * @return array List of popular domains. - */ - public function fetchPopularDomains(): array { - $url = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/popular_domains.json'; - try { - $response = $this->httpClient->request('GET', $url); - $data = json_decode($response->getBody()->getContents(), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); - } - - return $data; - } catch (RequestException $e) { - $this->logger->error('HTTP request failed: ' . $e->getMessage()); - throw new \RuntimeException('Failed to fetch popular domains.'); - } catch (\Throwable $th) { - $this->logger->error('Unexpected error: ' . $th->getMessage()); - throw $th; - } - } + public function __construct(Client $httpClient, ILogger $logger) { + $this->httpClient = $httpClient; + $this->logger = $logger; + } + + /** + * Fetch popular email domains from an external source. + * + * @return array List of popular domains. + */ + public function fetchPopularDomains(): array { + $url = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/popular_domains.json'; + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); + } + + return $data; + } catch (RequestException $e) { + $this->logger->error('HTTP request failed: ' . $e->getMessage()); + throw new \RuntimeException('Failed to fetch popular domains.'); + } catch (\Throwable $th) { + $this->logger->error('Unexpected error: ' . $th->getMessage()); + throw $th; + } + } } -- GitLab From dcdf8acec08aba741a07f871b2e91b9d938139bb Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 13:14:11 +0530 Subject: [PATCH 15/74] added app data --- lib/Command/CreatePopularDomain.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 63c900e..b08f5db 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -7,6 +7,7 @@ namespace OCA\EmailRecovery\Command; use OCA\EmailRecovery\AppInfo\Application; use OCA\EmailRecovery\Service\PopularDomainService; use OCP\ILogger; +use OCP\Files\IAppData; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -14,11 +15,13 @@ use Symfony\Component\Console\Output\OutputInterface; class CreatePopularDomain extends Command { private PopularDomainService $domainService; private ILogger $logger; + private IAppData $appData; - public function __construct(PopularDomainService $domainService, ILogger $logger) { + public function __construct(PopularDomainService $domainService, ILogger $logger, IAppData $appData) { parent::__construct(); $this->domainService = $domainService; $this->logger = $logger; + $this->appData = $appData; } protected function configure() { -- GitLab From 9ab4f897f22742441ef196624419f4324db9ea47 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 13:25:06 +0530 Subject: [PATCH 16/74] lint fix --- lib/Command/CreatePopularDomain.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index b08f5db..f2b1681 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -15,13 +15,13 @@ use Symfony\Component\Console\Output\OutputInterface; class CreatePopularDomain extends Command { private PopularDomainService $domainService; private ILogger $logger; - private IAppData $appData; + private IAppData $appData; public function __construct(PopularDomainService $domainService, ILogger $logger, IAppData $appData) { parent::__construct(); $this->domainService = $domainService; $this->logger = $logger; - $this->appData = $appData; + $this->appData = $appData; } protected function configure() { -- GitLab From a89b7389ca57ca26915b5ea03b55fb7a66d073e2 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 15:12:37 +0530 Subject: [PATCH 17/74] addec check for disposable domain --- lib/Command/CreatePopularDomain.php | 7 ++++--- lib/Service/PopularDomainService.php | 2 +- lib/Service/RecoveryEmailService.php | 11 ----------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index f2b1681..a9b9a0f 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -16,6 +16,7 @@ class CreatePopularDomain extends Command { private PopularDomainService $domainService; private ILogger $logger; private IAppData $appData; + private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; public function __construct(PopularDomainService $domainService, ILogger $logger, IAppData $appData) { parent::__construct(); @@ -44,18 +45,18 @@ class CreatePopularDomain extends Command { return Command::SUCCESS; } - private function getPopularDomainsFile(): \OCP\Files\ISimpleFile { + private function getPopularDomainsFile() { try { $currentFolder = $this->appData->getFolder('/'); } catch (NotFoundException $e) { $currentFolder = $this->appData->newFolder('/'); } - + $filename = self::POPULAR_DOMAINS_FILE_NAME; if ($currentFolder->fileExists($filename)) { return $currentFolder->getFile($filename); } - + return $currentFolder->newFile($filename); } } diff --git a/lib/Service/PopularDomainService.php b/lib/Service/PopularDomainService.php index 417ad2c..132e240 100644 --- a/lib/Service/PopularDomainService.php +++ b/lib/Service/PopularDomainService.php @@ -26,7 +26,7 @@ class PopularDomainService { * @return array List of popular domains. */ public function fetchPopularDomains(): array { - $url = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/popular_domains.json'; + $url = self::POPULAR_DOMAINS_URL; try { $response = $this->httpClient->request('GET', $url); $data = json_decode($response->getBody()->getContents(), true); diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index ed056fd..01a5bb8 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -375,15 +375,4 @@ class RecoveryEmailService { throw new \Exception("Rate limit exceeded. Please try again later."); } - - private function isDomainInCustomBlacklist(string $domain): bool { - $filePath = __DIR__ . '/../data/custom_disposable_domains.json'; - - if (!file_exists($filePath)) { - return false; // If file doesn't exist, the domain is not blacklisted - } - - $blacklistedDomains = json_decode(file_get_contents($filePath), true); - return in_array($domain, $blacklistedDomains, true); - } } -- GitLab From 113ddea101ea78fee5983b6c1bd4f1ed987af4ff Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 13 Jan 2025 18:42:21 +0530 Subject: [PATCH 18/74] added check for disposable domain --- lib/Service/BlackListService.php | 32 ++++++++++++++++++++++++++++ lib/Service/RecoveryEmailService.php | 1 + 2 files changed, 33 insertions(+) diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php index aaf463d..b66e306 100644 --- a/lib/Service/BlackListService.php +++ b/lib/Service/BlackListService.php @@ -221,4 +221,36 @@ class BlackListService { $emailDomain = strtolower(end($emailParts)); return in_array($emailDomain, $customlistedDomains); } + public function addToCustomDisposableDomains(string $domain, array $relatedDomains = []): void { + $document = self::DISPOSABLE_DOMAINS_FILE_NAME; + $file = $this->getDomainsFile($document); + + try { + $blacklistedDomains = []; + $blacklistedDomainsInJson = $file->getContent(); + + if (!empty($blacklistedDomainsInJson)) { + $blacklistedDomains = json_decode($blacklistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); + } + + // Merge new domains into the list + $newDomains = array_merge([$domain], $relatedDomains); + $blacklistedDomains = array_unique(array_merge($blacklistedDomains, $newDomains)); + + $encodedContent = json_encode($blacklistedDomains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($encodedContent === false) { + $this->logger->warning('Failed to encode domains to JSON for file ' . $document); + return; + } + + $file->putContent($encodedContent); + $this->logger->info('Added domain ' . $domain . ' and related domains to ' . $document); + } catch (\JsonException $e) { + $this->logger->warning('Error decoding JSON in disposable domains file ' . $document . ': ' . $e->getMessage()); + } catch (NotFoundException $e) { + $this->logger->warning('Disposable domains file ' . $document . ' not found!'); + } catch (\Throwable $e) { + $this->logger->warning('Unexpected error while processing disposable domains file ' . $document . ': ' . $e->getMessage()); + } + } } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 01a5bb8..0d7b678 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -173,6 +173,7 @@ class RecoveryEmailService { throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); } elseif (!$data['mx'] ?? true) { $this->logger->info("User ID $username's requested recovery email address domain is not valid."); + $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); } else { $this->logger->info("User ID $username's requested recovery email address domain is valid."); -- GitLab From 6a9322f08dcf3fc29ed5dc0261ac0b39d54fdf78 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Tue, 14 Jan 2025 12:45:59 +0530 Subject: [PATCH 19/74] applied suggestions --- lib/Service/BlackListService.php | 31 ++--- lib/Service/PopularDomainService.php | 47 +++++++- lib/Service/RecoveryEmailService.php | 164 ++++++++++++++++----------- 3 files changed, 147 insertions(+), 95 deletions(-) diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php index b66e306..91b84c7 100644 --- a/lib/Service/BlackListService.php +++ b/lib/Service/BlackListService.php @@ -17,7 +17,6 @@ class BlackListService { private $appName; private const BLACKLISTED_DOMAINS_FILE_NAME = 'blacklisted_domains.json'; private const BLACKLISTED_DOMAINS_URL = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'; - private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; private const DISPOSABLE_DOMAINS_FILE_NAME = 'disposable-domains.json'; public function __construct(string $appName, ILogger $logger, IFactory $l10nFactory, IAppData $appData) { $this->logger = $logger; @@ -157,29 +156,6 @@ class BlackListService { return $currentFolder->newFile($filename); } - /** - * Retrieve the Popular domain data. - * - * @return array The array of popular domains. - */ - public function getPopularlistedDomainData(): array { - $document = self::POPULAR_DOMAINS_FILE_NAME; - $file = $this->getDomainsFile($document); - try { - $popularlistedDomainsInJson = $file->getContent(); - if (empty($popularlistedDomainsInJson)) { - return []; - } - return json_decode($popularlistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); - } catch (NotFoundException $e) { - $this->logger->warning('Popular domains file ' . $document . ' not found!'); - return []; - } catch (\Throwable $e) { - $this->logger->warning('Error decoding Popular domains file ' . $document . ': ' . $e->getMessage()); - return []; - } - } - /** * Retrieve the Disposable domain data. * @@ -221,6 +197,13 @@ class BlackListService { $emailDomain = strtolower(end($emailParts)); return in_array($emailDomain, $customlistedDomains); } + /** + * Add a domain and its related domains to the custom disposable domains list. + * + * @param string $domain The domain to add to the custom disposable domains list. + * @param array $relatedDomains An optional array of related domains to add. + * @return void + */ public function addToCustomDisposableDomains(string $domain, array $relatedDomains = []): void { $document = self::DISPOSABLE_DOMAINS_FILE_NAME; $file = $this->getDomainsFile($document); diff --git a/lib/Service/PopularDomainService.php b/lib/Service/PopularDomainService.php index 132e240..286ffb3 100644 --- a/lib/Service/PopularDomainService.php +++ b/lib/Service/PopularDomainService.php @@ -7,17 +7,19 @@ namespace OCA\EmailRecovery\Service; use OCP\ILogger; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use OCP\Files\NotFoundException; class PopularDomainService { private Client $httpClient; private ILogger $logger; - + private BlackListService $blacklistService; private const POPULAR_DOMAINS_URL = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/domains.json'; private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; - public function __construct(Client $httpClient, ILogger $logger) { + public function __construct(Client $httpClient, ILogger $logger, BlackListService $blacklistService) { $this->httpClient = $httpClient; $this->logger = $logger; + $this->blacklistService = $blacklistService; } /** @@ -44,4 +46,45 @@ class PopularDomainService { throw $th; } } + /** + * Retrieve the Popular domain data. + * + * @return array The array of popular domains. + */ + public function getPopularlistedDomainData(): array { + $document = self::POPULAR_DOMAINS_FILE_NAME; + $file = $this->blacklistService->getDomainsFile($document); + try { + $popularlistedDomainsInJson = $file->getContent(); + if (empty($popularlistedDomainsInJson)) { + return []; + } + return json_decode($popularlistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); + } catch (NotFoundException $e) { + $this->logger->warning('Popular domains file ' . $document . ' not found!'); + return []; + } catch (\Throwable $e) { + $this->logger->warning('Error decoding Popular domains file ' . $document . ': ' . $e->getMessage()); + return []; + } + } + + /** + * Check if an domain is popular domain a JSON list of popular domains. + * + * @param string $email The email address to check. + * @return bool True if the email domain is popular, false otherwise. + */ + public function isPopularDomain(string $email): bool { + if (!$this->blacklistService->ensureDocumentsFolder()) { + return false; + } + $popularlistedDomains = $this->getPopularlistedDomainData(); + if (empty($popularlistedDomains)) { + return false; + } + $emailParts = explode('@', $email); + $emailDomain = strtolower(end($emailParts)); + return in_array($emailDomain, $popularlistedDomains); + } } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 0d7b678..5b37da5 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -39,6 +39,7 @@ class RecoveryEmailService { private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; + private const VERIFYMAIL_API_URL = 'https://verifymail.io/api/%s?key=%s'; private BlackListService $blackListService; private IL10N $l; private ISession $session; @@ -109,79 +110,104 @@ class RecoveryEmailService { return true; } - public function validateRecoveryEmail(string $recoveryEmail, string $username = '', string $language = 'en') : bool { - $email = ''; - if ($username != '') { - $user = $this->userManager->get($username); - $email = $user->getEMailAddress(); - } + public function validateRecoveryEmail(string $recoveryEmail, string $username = '', string $language = 'en'): bool { + // Fetch user email if username is provided + $email = $this->getUserEmail($username); + $l = $this->l10nFactory->get($this->appName, $language); - if (!empty($recoveryEmail)) { - if (!$this->isValidEmailFormat($recoveryEmail)) { - $this->logger->info("User $username's requested recovery email does not match email format"); - throw new InvalidRecoveryEmailException($l->t('Invalid Recovery Email')); - } - if ($email != '' && strcmp($recoveryEmail, $email) === 0) { - $this->logger->info("User ID $username's requested recovery email is the same as email"); - throw new SameRecoveryEmailAsEmailException($l->t('Error! User email address cannot be saved as recovery email address!')); - } - if ($this->isRecoveryEmailTaken($username, $recoveryEmail)) { - $this->logger->info("User ID $username's requested recovery email address is already taken"); - throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.')); - } - if ($this->isRecoveryEmailDomainDisallowed($recoveryEmail)) { - $this->logger->info("User ID $username's requested recovery email address is disallowed."); - throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); - } - if ($this->blackListService->isBlacklistedEmail($recoveryEmail)) { - $this->logger->info("User ID $username's requested recovery email address domain is blacklisted. Please provide another recovery address."); - throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); - } - $apiKey = getenv('VERIFYMAIL_API_KEY'); - - if ($this->blackListService->isPopularDomain($recoveryEmail) && $this->blackListService->isDomainInCustomDisposable($recoveryEmail)) { - // Ensure rate limit for email verification (2 requests per second) - $this->ensureRealTimeRateLimit('rate_limit_email', 2); - - // Step 3.1: Verify the email address using verifymail.io - $url = "https://verifymail.io/api/$recoveryEmail?key=$apiKey"; - $response = file_get_contents($url); - $data = json_decode($response, true); - - if ($data['disposable'] ?? false) { - $this->logger->info("User ID $username's requested recovery email address is disposable."); - throw new BlacklistedEmailException("The email address is disposable. Please provide another recovery address."); - } elseif (!$data['deliverable_email'] ?? true) { - $this->logger->info("User ID $username's requested recovery email address is not deliverable."); - throw new BlacklistedEmailException("The email address is not deliverable. Please provide another recovery address."); - } else { - $this->logger->info("User ID $username's requested recovery email address is valid and not disposable."); - return true; // Email is valid - } - } - - $this->ensureRealTimeRateLimit('rate_limit_domain', 15); - $domain = substr(strrchr($recoveryEmail, "@"), 1); - $url = "https://verifymail.io/api/$domain?key=$apiKey"; - $response = file_get_contents($url); - $data = json_decode($response, true); - - if ($data['disposable'] ?? false) { - $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); - // Store domain and related domains in custom_disposable_domains.json - $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); - throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); - } elseif (!$data['mx'] ?? true) { - $this->logger->info("User ID $username's requested recovery email address domain is not valid."); - $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); - throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); - } else { - $this->logger->info("User ID $username's requested recovery email address domain is valid."); - return true; // Domain and email are valid - } + $this->validateBasicRecoveryEmailRules($recoveryEmail, $username, $email, $l); + + $apiKey = getenv('VERIFYMAIL_API_KEY'); + + // Check if it's a popular domain and custom disposable, then verify the email + if ($this->blackListService->isPopularDomain($recoveryEmail) && $this->blackListService->isDomainInCustomDisposable($recoveryEmail)) { + $this->ensureRealTimeRateLimit('rate_limit_email', 2); + $this->verifyEmailWithApi($recoveryEmail, $username, $apiKey); } + + // Verify the domain using the API + $this->ensureRealTimeRateLimit('rate_limit_domain', 15); + $domain = substr(strrchr($recoveryEmail, "@"), 1); + $this->verifyDomainWithApi($domain, $username, $apiKey); + return true; } + private function getUserEmail(string $username): string { + if (empty($username)) { + return ''; + } + $user = $this->userManager->get($username); + return $user->getEMailAddress(); + } + private function validateBasicRecoveryEmailRules(string $recoveryEmail, string $username, string $email, IL10N $l): void { + if (empty($recoveryEmail)) { + return; + } + + if (!$this->isValidEmailFormat($recoveryEmail)) { + $this->logger->info("User $username's requested recovery email does not match email format"); + throw new InvalidRecoveryEmailException($l->t('Invalid Recovery Email')); + } + + if (!empty($email) && strcmp($recoveryEmail, $email) === 0) { + $this->logger->info("User ID $username's requested recovery email is the same as email"); + throw new SameRecoveryEmailAsEmailException($l->t('Error! User email address cannot be saved as recovery email address!')); + } + + if ($this->isRecoveryEmailTaken($username, $recoveryEmail)) { + $this->logger->info("User ID $username's requested recovery email address is already taken"); + throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.')); + } + + if ($this->isRecoveryEmailDomainDisallowed($recoveryEmail)) { + $this->logger->info("User ID $username's requested recovery email address is disallowed."); + throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); + } + + if ($this->blackListService->isBlacklistedEmail($recoveryEmail)) { + $this->logger->info("User ID $username's requested recovery email address domain is blacklisted."); + throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); + } + } + + private function verifyEmailWithApi(string $recoveryEmail, string $username, string $apiKey): void { + $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); + $response = file_get_contents($url); + $data = json_decode($response, true); + + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is disposable."); + throw new BlacklistedEmailException("The email address is disposable. Please provide another recovery address."); + } + + if (!$data['deliverable_email'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address is not deliverable."); + throw new BlacklistedEmailException("The email address is not deliverable. Please provide another recovery address."); + } + + $this->logger->info("User ID $username's requested recovery email address is valid and not disposable."); + } + + private function verifyDomainWithApi(string $domain, string $username, string $apiKey): void { + $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); + $response = file_get_contents($url); + $data = json_decode($response, true); + + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); + $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); + } + + if (!$data['mx'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address domain is not valid."); + $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); + } + + $this->logger->info("User ID $username's requested recovery email address domain is valid."); + } + public function isRecoveryEmailDomainDisallowed(string $recoveryEmail): bool { $recoveryEmail = strtolower($recoveryEmail); $emailParts = explode('@', $recoveryEmail); -- GitLab From 4a7a0b5b2ea25c4751ccd1b46bda3a20b47c2607 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Tue, 14 Jan 2025 13:21:55 +0530 Subject: [PATCH 20/74] applied suggestions --- lib/Command/CreatePopularDomain.php | 2 +- lib/Service/BlackListService.php | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index a9b9a0f..d66782a 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -36,7 +36,7 @@ class CreatePopularDomain extends Command { $domains = $this->domainService->fetchPopularDomains(); $file = $this->getPopularDomainsFile(); $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - $output->writeln('Popular domains list created successfully at ' . $filePath); + $output->writeln('Popular domains list created successfully.'); } catch (\Throwable $th) { $this->logger->error('Error while fetching popular domains. ' . $th->getMessage()); $output->writeln('Error while fetching popular domains: ' . $th->getMessage()); diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php index 91b84c7..0c419fb 100644 --- a/lib/Service/BlackListService.php +++ b/lib/Service/BlackListService.php @@ -14,15 +14,17 @@ class BlackListService { private ILogger $logger; protected IFactory $l10nFactory; private IAppData $appData; + private PopularDomainService $popularDomainService; private $appName; private const BLACKLISTED_DOMAINS_FILE_NAME = 'blacklisted_domains.json'; private const BLACKLISTED_DOMAINS_URL = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'; private const DISPOSABLE_DOMAINS_FILE_NAME = 'disposable-domains.json'; - public function __construct(string $appName, ILogger $logger, IFactory $l10nFactory, IAppData $appData) { + public function __construct(string $appName, ILogger $logger, IFactory $l10nFactory, IAppData $appData, PopularDomainService $popularDomainService) { $this->logger = $logger; $this->l10nFactory = $l10nFactory; $this->appData = $appData; $this->appName = $appName; + $this->popularDomainService = $popularDomainService; } /** * Check if an email domain is blacklisted against a JSON list of disposable email domains. @@ -130,7 +132,7 @@ class BlackListService { if (!$this->ensureDocumentsFolder()) { return false; } - $popularlistedDomains = $this->getPopularlistedDomainData(); + $popularlistedDomains = $this->popularDomainService->getPopularlistedDomainData(); if (empty($popularlistedDomains)) { return false; } -- GitLab From 7c8dcab2e54db091a88dbdd119117d7024f369df Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Wed, 15 Jan 2025 19:44:11 +0530 Subject: [PATCH 21/74] fix the timestamp --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 5b37da5..1bc7096 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -386,7 +386,7 @@ class RecoveryEmailService { // Remove old requests (keep only those within a 1-second window) $requests = array_filter($requests, function ($timestamp) use ($now) { - return ($now - $timestamp) < 1; + return ($now - $timestamp) <= 1; }); if (count($requests) < $rateLimit) { -- GitLab From b91755a9b8f2dfa1c965e0f633e18e1af83f2560 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Wed, 15 Jan 2025 19:49:43 +0530 Subject: [PATCH 22/74] used constant instead of string in method --- lib/Service/RecoveryEmailService.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 1bc7096..fb7a71d 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -40,6 +40,9 @@ class RecoveryEmailService { protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; private const VERIFYMAIL_API_URL = 'https://verifymail.io/api/%s?key=%s'; + private const RATE_LIMIT_EMAIL = 'verifymail_email_ratelimit'; + private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; + private BlackListService $blackListService; private IL10N $l; private ISession $session; @@ -121,12 +124,12 @@ class RecoveryEmailService { // Check if it's a popular domain and custom disposable, then verify the email if ($this->blackListService->isPopularDomain($recoveryEmail) && $this->blackListService->isDomainInCustomDisposable($recoveryEmail)) { - $this->ensureRealTimeRateLimit('rate_limit_email', 2); + $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); $this->verifyEmailWithApi($recoveryEmail, $username, $apiKey); } // Verify the domain using the API - $this->ensureRealTimeRateLimit('rate_limit_domain', 15); + $this->ensureRealTimeRateLimit(self::RATE_LIMIT_DOMAIN, 15); $domain = substr(strrchr($recoveryEmail, "@"), 1); $this->verifyDomainWithApi($domain, $username, $apiKey); -- GitLab From 3f86516d897ac0535d68c10a87c63ad8b1c0b82f Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Wed, 15 Jan 2025 21:30:11 +0530 Subject: [PATCH 23/74] added domain service --- lib/Service/BlackListService.php | 241 --------------------------- lib/Service/DomainService.php | 147 ++++++++++++++++ lib/Service/PopularDomainService.php | 90 ---------- lib/Service/RecoveryEmailService.php | 20 +-- 4 files changed, 157 insertions(+), 341 deletions(-) delete mode 100644 lib/Service/BlackListService.php create mode 100644 lib/Service/DomainService.php delete mode 100644 lib/Service/PopularDomainService.php diff --git a/lib/Service/BlackListService.php b/lib/Service/BlackListService.php deleted file mode 100644 index 0c419fb..0000000 --- a/lib/Service/BlackListService.php +++ /dev/null @@ -1,241 +0,0 @@ -logger = $logger; - $this->l10nFactory = $l10nFactory; - $this->appData = $appData; - $this->appName = $appName; - $this->popularDomainService = $popularDomainService; - } - /** - * Check if an email domain is blacklisted against a JSON list of disposable email domains. - * - * @param string $email The email address to check. - * @return bool True if the email domain is blacklisted, false otherwise. - */ - public function isBlacklistedEmail(string $email): bool { - if (!$this->ensureDocumentsFolder()) { - return false; - } - $blacklistedDomains = $this->getBlacklistedDomainData(); - if (empty($blacklistedDomains)) { - return false; - } - $emailParts = explode('@', $email); - $emailDomain = strtolower(end($emailParts)); - return in_array($emailDomain, $blacklistedDomains); - } - /** - * Update the blacklisted domains data by fetching it from a URL and saving it locally. - * - * @return void - */ - public function updateBlacklistedDomains(): void { - $blacklisted_domain_url = self::BLACKLISTED_DOMAINS_URL; - $json_data = file_get_contents($blacklisted_domain_url); - $this->setBlacklistedDomainsData($json_data); - } - /** - * Store blacklisted domain data in a file within AppData. - * - * @param string $data The data to be stored in the file. - * @return void - */ - private function setBlacklistedDomainsData(string $data): void { - $file = $this->getBlacklistedDomainsFile(); - $file->putContent($data); - } - /** - * Retrieve the blacklisted domain file path - * - * @return ISimpleFile - */ - private function getBlacklistedDomainsFile(): ISimpleFile { - try { - $currentFolder = $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $currentFolder = $this->appData->newFolder('/'); - } - $filename = self::BLACKLISTED_DOMAINS_FILE_NAME; - if ($currentFolder->fileExists($filename)) { - return $currentFolder->getFile($filename); - } - return $currentFolder->newFile($filename); - } - /** - * Retrieve the blacklisted domain data. - * - * @return array The array of blacklisted domains. - */ - public function getBlacklistedDomainData(): array { - $document = self::BLACKLISTED_DOMAINS_FILE_NAME; - $file = $this->getBlacklistedDomainsFile(); - try { - $blacklistedDomainsInJson = $file->getContent(); - if (empty($blacklistedDomainsInJson)) { - return []; - } - return json_decode($blacklistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); - } catch (NotFoundException $e) { - $this->logger->warning('Blacklisted domains file ' . $document . ' not found!'); - return []; - } catch (\Throwable $e) { - $this->logger->warning('Error decoding blacklisted domains file ' . $document . ': ' . $e->getMessage()); - return []; - } - } - - /** - * Ensure the specified folder exists within AppData. - * - * @return bool - */ - private function ensureDocumentsFolder(): bool { - try { - $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $this->logger->error($this->appName . ' AppData folder not found!'); - return false; - } catch (\RuntimeException $e) { - $this->logger->error($this->appName . ' AppData folder not found! Runtime Error: ' . $e->getMessage()); - return false; - } - return true; - } - - /** - * Check if an domain is popular domain a JSON list of popular domains. - * - * @param string $email The email address to check. - * @return bool True if the email domain is popular, false otherwise. - */ - public function isPopularDomain(string $email): bool { - if (!$this->ensureDocumentsFolder()) { - return false; - } - $popularlistedDomains = $this->popularDomainService->getPopularlistedDomainData(); - if (empty($popularlistedDomains)) { - return false; - } - $emailParts = explode('@', $email); - $emailDomain = strtolower(end($emailParts)); - return in_array($emailDomain, $popularlistedDomains); - } - - /** - * Retrieve the domain file - * - * @return ISimpleFile - */ - private function getDomainsFile($filename): ISimpleFile { - try { - $currentFolder = $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $currentFolder = $this->appData->newFolder('/'); - } - if ($currentFolder->fileExists($filename)) { - return $currentFolder->getFile($filename); - } - return $currentFolder->newFile($filename); - } - - /** - * Retrieve the Disposable domain data. - * - * @return array The array of disposable domains. - */ - public function getDisposablelistedDomainData(): array { - $document = self::DISPOSABLE_DOMAINS_FILE_NAME; - $file = $this->getDomainsFile($document); - try { - $disposablelistedDomainsInJson = $file->getContent(); - if (empty($disposablelistedDomainsInJson)) { - return []; - } - return json_decode($disposablelistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); - } catch (NotFoundException $e) { - $this->logger->warning('Disposable domains file ' . $document . ' not found!'); - return []; - } catch (\Throwable $e) { - $this->logger->warning('Error decoding Disposable domains file ' . $document . ': ' . $e->getMessage()); - return []; - } - } - - /** - * Check if an domain is custom disposable domain a JSON list of disposable domains. - * - * @param string $email The email address to check. - * @return bool True if the email domain is custom disposable domain, false otherwise. - */ - public function isDomainInCustomDisposable(string $email): bool { - if (!$this->ensureDocumentsFolder()) { - return false; - } - $customlistedDomains = $this->getDisposablelistedDomainData(); - if (empty($customlistedDomains)) { - return false; - } - $emailParts = explode('@', $email); - $emailDomain = strtolower(end($emailParts)); - return in_array($emailDomain, $customlistedDomains); - } - /** - * Add a domain and its related domains to the custom disposable domains list. - * - * @param string $domain The domain to add to the custom disposable domains list. - * @param array $relatedDomains An optional array of related domains to add. - * @return void - */ - public function addToCustomDisposableDomains(string $domain, array $relatedDomains = []): void { - $document = self::DISPOSABLE_DOMAINS_FILE_NAME; - $file = $this->getDomainsFile($document); - - try { - $blacklistedDomains = []; - $blacklistedDomainsInJson = $file->getContent(); - - if (!empty($blacklistedDomainsInJson)) { - $blacklistedDomains = json_decode($blacklistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); - } - - // Merge new domains into the list - $newDomains = array_merge([$domain], $relatedDomains); - $blacklistedDomains = array_unique(array_merge($blacklistedDomains, $newDomains)); - - $encodedContent = json_encode($blacklistedDomains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($encodedContent === false) { - $this->logger->warning('Failed to encode domains to JSON for file ' . $document); - return; - } - - $file->putContent($encodedContent); - $this->logger->info('Added domain ' . $domain . ' and related domains to ' . $document); - } catch (\JsonException $e) { - $this->logger->warning('Error decoding JSON in disposable domains file ' . $document . ': ' . $e->getMessage()); - } catch (NotFoundException $e) { - $this->logger->warning('Disposable domains file ' . $document . ' not found!'); - } catch (\Throwable $e) { - $this->logger->warning('Unexpected error while processing disposable domains file ' . $document . ': ' . $e->getMessage()); - } - } -} diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php new file mode 100644 index 0000000..54fe5be --- /dev/null +++ b/lib/Service/DomainService.php @@ -0,0 +1,147 @@ +logger = $logger; + $this->appData = $appData; + $this->httpClient = $httpClient; + $this->appName = $appName; + } + + // ------------------------------- + // Public Methods + // ------------------------------- + + /** + * Check if an email belongs to a popular domain. + */ + public function isPopularDomain(string $email): bool { + $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE); + return $this->isDomainInList($email, $domains); + } + + /** + * Check if an email belongs to a blacklisted domain. + */ + public function isBlacklistedDomain(string $email): bool { + $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE); + return $this->isDomainInList($email, $domains); + } + + /** + * Check if an email belongs to a disposable domain. + */ + public function isDisposableDomain(string $email): bool { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); + return $this->isDomainInList($email, $domains); + } + + /** + * Update blacklisted domains by fetching from the external source. + */ + public function updateBlacklistedDomains(): void { + $this->updateDomainsFromUrl(self::BLACKLISTED_DOMAINS_URL, self::BLACKLISTED_DOMAINS_FILE); + } + + /** + * Add a custom disposable domain to the list. + */ + public function addCustomDisposableDomain(string $domain, array $relatedDomains = []): void { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); + $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); + $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); + } + + // ------------------------------- + // Private Helper Methods + // ------------------------------- + + /** + * Check if an email's domain is in the given list of domains. + */ + private function isDomainInList(string $email, array $domains): bool { + if (empty($domains)) { + return false; + } + $emailDomain = strtolower(explode('@', $email)[1]); + return in_array($emailDomain, $domains, true); + } + + /** + * Fetch and update domains from an external URL. + */ + private function updateDomainsFromUrl(string $url, string $filename): void { + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + $this->saveDomainsToFile($filename, $data); + } catch (RequestException $e) { + $this->logger->error("Failed to fetch domains from $url: " . $e->getMessage()); + } catch (\Throwable $e) { + $this->logger->error("Unexpected error while updating domains: " . $e->getMessage()); + } + } + + /** + * Get domains from a file. + */ + private function getDomainsFromFile(string $filename): array { + try { + $file = $this->getDomainsFile($filename); + $content = $file->getContent(); + return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; + } catch (NotFoundException $e) { + $this->logger->warning("File $filename not found!"); + return []; + } catch (\Throwable $e) { + $this->logger->warning("Error reading $filename: " . $e->getMessage()); + return []; + } + } + + /** + * Save domains to a file. + */ + private function saveDomainsToFile(string $filename, array $domains): void { + $file = $this->getDomainsFile($filename); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + /** + * Get or create a file in AppData. + */ + private function getDomainsFile(string $filename): ISimpleFile { + try { + $folder = $this->appData->getFolder('/'); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('/'); + } + + if ($folder->fileExists($filename)) { + return $folder->getFile($filename); + } + return $folder->newFile($filename); + } +} diff --git a/lib/Service/PopularDomainService.php b/lib/Service/PopularDomainService.php deleted file mode 100644 index 286ffb3..0000000 --- a/lib/Service/PopularDomainService.php +++ /dev/null @@ -1,90 +0,0 @@ -httpClient = $httpClient; - $this->logger = $logger; - $this->blacklistService = $blacklistService; - } - - /** - * Fetch popular email domains from an external source. - * - * @return array List of popular domains. - */ - public function fetchPopularDomains(): array { - $url = self::POPULAR_DOMAINS_URL; - try { - $response = $this->httpClient->request('GET', $url); - $data = json_decode($response->getBody()->getContents(), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); - } - - return $data; - } catch (RequestException $e) { - $this->logger->error('HTTP request failed: ' . $e->getMessage()); - throw new \RuntimeException('Failed to fetch popular domains.'); - } catch (\Throwable $th) { - $this->logger->error('Unexpected error: ' . $th->getMessage()); - throw $th; - } - } - /** - * Retrieve the Popular domain data. - * - * @return array The array of popular domains. - */ - public function getPopularlistedDomainData(): array { - $document = self::POPULAR_DOMAINS_FILE_NAME; - $file = $this->blacklistService->getDomainsFile($document); - try { - $popularlistedDomainsInJson = $file->getContent(); - if (empty($popularlistedDomainsInJson)) { - return []; - } - return json_decode($popularlistedDomainsInJson, true, 512, JSON_THROW_ON_ERROR); - } catch (NotFoundException $e) { - $this->logger->warning('Popular domains file ' . $document . ' not found!'); - return []; - } catch (\Throwable $e) { - $this->logger->warning('Error decoding Popular domains file ' . $document . ': ' . $e->getMessage()); - return []; - } - } - - /** - * Check if an domain is popular domain a JSON list of popular domains. - * - * @param string $email The email address to check. - * @return bool True if the email domain is popular, false otherwise. - */ - public function isPopularDomain(string $email): bool { - if (!$this->blacklistService->ensureDocumentsFolder()) { - return false; - } - $popularlistedDomains = $this->getPopularlistedDomainData(); - if (empty($popularlistedDomains)) { - return false; - } - $emailParts = explode('@', $email); - $emailDomain = strtolower(end($emailParts)); - return in_array($emailDomain, $popularlistedDomains); - } -} diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index fb7a71d..6d20e36 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -43,11 +43,11 @@ class RecoveryEmailService { private const RATE_LIMIT_EMAIL = 'verifymail_email_ratelimit'; private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; - private BlackListService $blackListService; + private DomainService $domainService; private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, BlackListService $blackListService, IL10N $l) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -59,10 +59,10 @@ class RecoveryEmailService { $this->themingDefaults = $themingDefaults; $this->verificationToken = $verificationToken; $this->curl = $curlService; - $this->blackListService = $blackListService; + $this->domainService = $domainService; $this->l = $l; // Initialize Redis cache directly - $this->redisCache = \OC::$server->getMemCacheFactory()->createDistributed(); + $this->cache = \OC::$server->getMemCacheFactory()->createDistributed(); $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { @@ -123,7 +123,7 @@ class RecoveryEmailService { $apiKey = getenv('VERIFYMAIL_API_KEY'); // Check if it's a popular domain and custom disposable, then verify the email - if ($this->blackListService->isPopularDomain($recoveryEmail) && $this->blackListService->isDomainInCustomDisposable($recoveryEmail)) { + if ($this->domainService->isPopularDomain($recoveryEmail) && $this->domainService->isDomainInCustomDisposable($recoveryEmail)) { $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); $this->verifyEmailWithApi($recoveryEmail, $username, $apiKey); } @@ -167,7 +167,7 @@ class RecoveryEmailService { throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); } - if ($this->blackListService->isBlacklistedEmail($recoveryEmail)) { + if ($this->domainService->isBlacklistedEmail($recoveryEmail)) { $this->logger->info("User ID $username's requested recovery email address domain is blacklisted."); throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } @@ -198,13 +198,13 @@ class RecoveryEmailService { if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); - $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + $this->domainService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); } if (!$data['mx'] ?? true) { $this->logger->info("User ID $username's requested recovery email address domain is not valid."); - $this->blackListService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + $this->domainService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); } @@ -385,7 +385,7 @@ class RecoveryEmailService { $now = microtime(true); for ($retry = 0; $retry < $maxRetries; $retry++) { - $requests = $this->redisCache->get($key) ?? []; + $requests = $this->cache->get($key) ?? []; // Remove old requests (keep only those within a 1-second window) $requests = array_filter($requests, function ($timestamp) use ($now) { @@ -395,7 +395,7 @@ class RecoveryEmailService { if (count($requests) < $rateLimit) { // Under the rate limit, add the current request timestamp and proceed $requests[] = $now; - $this->redisCache->set($key, $requests, 2); // Set expiry of 2 seconds for cached timestamps + $this->cache->set($key, $requests, 2); // Set expiry of 2 seconds for cached timestamps return; } -- GitLab From d8d951b631427be61496826fa86b4051145dcffb Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 09:59:54 +0530 Subject: [PATCH 24/74] lint fix --- lib/Service/DomainService.php | 264 +++++++++++++++++----------------- 1 file changed, 132 insertions(+), 132 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 54fe5be..2bb8176 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -12,136 +12,136 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; class DomainService { - private ILogger $logger; - private IAppData $appData; - private Client $httpClient; - private string $appName; - - private const POPULAR_DOMAINS_URL = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/domains.json'; - private const POPULAR_DOMAINS_FILE = 'popular_domains.json'; - private const BLACKLISTED_DOMAINS_URL = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'; - private const BLACKLISTED_DOMAINS_FILE = 'blacklisted_domains.json'; - private const DISPOSABLE_DOMAINS_FILE = 'disposable_domains.json'; - - public function __construct(string $appName, ILogger $logger, IAppData $appData, Client $httpClient) { - $this->logger = $logger; - $this->appData = $appData; - $this->httpClient = $httpClient; - $this->appName = $appName; - } - - // ------------------------------- - // Public Methods - // ------------------------------- - - /** - * Check if an email belongs to a popular domain. - */ - public function isPopularDomain(string $email): bool { - $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE); - return $this->isDomainInList($email, $domains); - } - - /** - * Check if an email belongs to a blacklisted domain. - */ - public function isBlacklistedDomain(string $email): bool { - $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE); - return $this->isDomainInList($email, $domains); - } - - /** - * Check if an email belongs to a disposable domain. - */ - public function isDisposableDomain(string $email): bool { - $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); - return $this->isDomainInList($email, $domains); - } - - /** - * Update blacklisted domains by fetching from the external source. - */ - public function updateBlacklistedDomains(): void { - $this->updateDomainsFromUrl(self::BLACKLISTED_DOMAINS_URL, self::BLACKLISTED_DOMAINS_FILE); - } - - /** - * Add a custom disposable domain to the list. - */ - public function addCustomDisposableDomain(string $domain, array $relatedDomains = []): void { - $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); - $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); - $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); - } - - // ------------------------------- - // Private Helper Methods - // ------------------------------- - - /** - * Check if an email's domain is in the given list of domains. - */ - private function isDomainInList(string $email, array $domains): bool { - if (empty($domains)) { - return false; - } - $emailDomain = strtolower(explode('@', $email)[1]); - return in_array($emailDomain, $domains, true); - } - - /** - * Fetch and update domains from an external URL. - */ - private function updateDomainsFromUrl(string $url, string $filename): void { - try { - $response = $this->httpClient->request('GET', $url); - $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - $this->saveDomainsToFile($filename, $data); - } catch (RequestException $e) { - $this->logger->error("Failed to fetch domains from $url: " . $e->getMessage()); - } catch (\Throwable $e) { - $this->logger->error("Unexpected error while updating domains: " . $e->getMessage()); - } - } - - /** - * Get domains from a file. - */ - private function getDomainsFromFile(string $filename): array { - try { - $file = $this->getDomainsFile($filename); - $content = $file->getContent(); - return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; - } catch (NotFoundException $e) { - $this->logger->warning("File $filename not found!"); - return []; - } catch (\Throwable $e) { - $this->logger->warning("Error reading $filename: " . $e->getMessage()); - return []; - } - } - - /** - * Save domains to a file. - */ - private function saveDomainsToFile(string $filename, array $domains): void { - $file = $this->getDomainsFile($filename); - $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } - - /** - * Get or create a file in AppData. - */ - private function getDomainsFile(string $filename): ISimpleFile { - try { - $folder = $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $folder = $this->appData->newFolder('/'); - } - - if ($folder->fileExists($filename)) { - return $folder->getFile($filename); - } - return $folder->newFile($filename); - } + private ILogger $logger; + private IAppData $appData; + private Client $httpClient; + private string $appName; + + private const POPULAR_DOMAINS_URL = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/domains.json'; + private const POPULAR_DOMAINS_FILE = 'popular_domains.json'; + private const BLACKLISTED_DOMAINS_URL = 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'; + private const BLACKLISTED_DOMAINS_FILE = 'blacklisted_domains.json'; + private const DISPOSABLE_DOMAINS_FILE = 'disposable_domains.json'; + + public function __construct(string $appName, ILogger $logger, IAppData $appData, Client $httpClient) { + $this->logger = $logger; + $this->appData = $appData; + $this->httpClient = $httpClient; + $this->appName = $appName; + } + + // ------------------------------- + // Public Methods + // ------------------------------- + + /** + * Check if an email belongs to a popular domain. + */ + public function isPopularDomain(string $email): bool { + $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE); + return $this->isDomainInList($email, $domains); + } + + /** + * Check if an email belongs to a blacklisted domain. + */ + public function isBlacklistedDomain(string $email): bool { + $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE); + return $this->isDomainInList($email, $domains); + } + + /** + * Check if an email belongs to a disposable domain. + */ + public function isDisposableDomain(string $email): bool { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); + return $this->isDomainInList($email, $domains); + } + + /** + * Update blacklisted domains by fetching from the external source. + */ + public function updateBlacklistedDomains(): void { + $this->updateDomainsFromUrl(self::BLACKLISTED_DOMAINS_URL, self::BLACKLISTED_DOMAINS_FILE); + } + + /** + * Add a custom disposable domain to the list. + */ + public function addCustomDisposableDomain(string $domain, array $relatedDomains = []): void { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); + $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); + $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); + } + + // ------------------------------- + // Private Helper Methods + // ------------------------------- + + /** + * Check if an email's domain is in the given list of domains. + */ + private function isDomainInList(string $email, array $domains): bool { + if (empty($domains)) { + return false; + } + $emailDomain = strtolower(explode('@', $email)[1]); + return in_array($emailDomain, $domains, true); + } + + /** + * Fetch and update domains from an external URL. + */ + private function updateDomainsFromUrl(string $url, string $filename): void { + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + $this->saveDomainsToFile($filename, $data); + } catch (RequestException $e) { + $this->logger->error("Failed to fetch domains from $url: " . $e->getMessage()); + } catch (\Throwable $e) { + $this->logger->error("Unexpected error while updating domains: " . $e->getMessage()); + } + } + + /** + * Get domains from a file. + */ + private function getDomainsFromFile(string $filename): array { + try { + $file = $this->getDomainsFile($filename); + $content = $file->getContent(); + return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; + } catch (NotFoundException $e) { + $this->logger->warning("File $filename not found!"); + return []; + } catch (\Throwable $e) { + $this->logger->warning("Error reading $filename: " . $e->getMessage()); + return []; + } + } + + /** + * Save domains to a file. + */ + private function saveDomainsToFile(string $filename, array $domains): void { + $file = $this->getDomainsFile($filename); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + + /** + * Get or create a file in AppData. + */ + private function getDomainsFile(string $filename): ISimpleFile { + try { + $folder = $this->appData->getFolder('/'); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('/'); + } + + if ($folder->fileExists($filename)) { + return $folder->getFile($filename); + } + return $folder->newFile($filename); + } } -- GitLab From 46b696e41b70c823e13fe727758a63cd201c9fbf Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 10:22:29 +0530 Subject: [PATCH 25/74] add to single class --- lib/Service/DomainService.php | 4 ++-- lib/Service/RecoveryEmailService.php | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 2bb8176..5460cf1 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -51,9 +51,9 @@ class DomainService { } /** - * Check if an email belongs to a disposable domain. + * Check if an email belongs to a custom blacklist domain. */ - public function isDisposableDomain(string $email): bool { + public function isDomainInCustomBlacklist(string $email): bool { $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); return $this->isDomainInList($email, $domains); } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 6d20e36..db58797 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -122,8 +122,8 @@ class RecoveryEmailService { $apiKey = getenv('VERIFYMAIL_API_KEY'); - // Check if it's a popular domain and custom disposable, then verify the email - if ($this->domainService->isPopularDomain($recoveryEmail) && $this->domainService->isDomainInCustomDisposable($recoveryEmail)) { + // Check if it's a popular domain and not custom blacklist, then verify the email + if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail)) { $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); $this->verifyEmailWithApi($recoveryEmail, $username, $apiKey); } @@ -167,7 +167,7 @@ class RecoveryEmailService { throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); } - if ($this->domainService->isBlacklistedEmail($recoveryEmail)) { + if ($this->domainService->isBlacklistedDomain($recoveryEmail)) { $this->logger->info("User ID $username's requested recovery email address domain is blacklisted."); throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } @@ -198,13 +198,13 @@ class RecoveryEmailService { if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); - $this->domainService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); } if (!$data['mx'] ?? true) { $this->logger->info("User ID $username's requested recovery email address domain is not valid."); - $this->domainService->addToCustomDisposableDomains($domain, $data['related_domains'] ?? []); + $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); } -- GitLab From 1d9d827510951cb8ed4c3c185c16694a2d9f97f6 Mon Sep 17 00:00:00 2001 From: AVINASH GUSAIN Date: Thu, 16 Jan 2025 06:43:59 +0000 Subject: [PATCH 26/74] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Fahim Salam Chowdhury --- lib/Service/RecoveryEmailService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index db58797..2548a5c 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -142,6 +142,7 @@ class RecoveryEmailService { $user = $this->userManager->get($username); return $user->getEMailAddress(); } + private function validateBasicRecoveryEmailRules(string $recoveryEmail, string $username, string $email, IL10N $l): void { if (empty($recoveryEmail)) { return; -- GitLab From d53227f47dfe4a04ec769352e38dbf8e2d330bf1 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 12:25:52 +0530 Subject: [PATCH 27/74] applied suggestion --- lib/Command/CreatePopularDomain.php | 6 +++--- lib/Service/DomainService.php | 12 ++++++++++++ lib/Service/RecoveryEmailService.php | 8 ++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index d66782a..76865cb 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -33,17 +33,17 @@ class CreatePopularDomain extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { try { - $domains = $this->domainService->fetchPopularDomains(); - $file = $this->getPopularDomainsFile(); - $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $this->domainService->updatePopularDomainsFile(); $output->writeln('Popular domains list created successfully.'); } catch (\Throwable $th) { $this->logger->error('Error while fetching popular domains. ' . $th->getMessage()); $output->writeln('Error while fetching popular domains: ' . $th->getMessage()); return Command::FAILURE; } + return Command::SUCCESS; } + private function getPopularDomainsFile() { try { diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 5460cf1..324ff79 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -144,4 +144,16 @@ class DomainService { } return $folder->newFile($filename); } + + public function updatePopularDomainsFile(): void { + try { + $domains = $this->fetchPopularDomains(); + $file = $this->getDomainsFile(self::POPULAR_DOMAINS_FILE); + $file->putContent(json_encode($domains, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $this->logger->info('Popular domains list updated successfully.'); + } catch (\Throwable $e) { + $this->logger->error('Error while updating popular domains file: ' . $e->getMessage()); + throw new \RuntimeException('Failed to update popular domains file.', 0, $e); + } + } } diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 2548a5c..c0edbf0 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -118,14 +118,14 @@ class RecoveryEmailService { $email = $this->getUserEmail($username); $l = $this->l10nFactory->get($this->appName, $language); - $this->validateBasicRecoveryEmailRules($recoveryEmail, $username, $email, $l); + $this->enforceBasicRecoveryEmailRules($recoveryEmail, $username, $email, $l); $apiKey = getenv('VERIFYMAIL_API_KEY'); // Check if it's a popular domain and not custom blacklist, then verify the email if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail)) { $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); - $this->verifyEmailWithApi($recoveryEmail, $username, $apiKey); + $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey); } // Verify the domain using the API @@ -143,7 +143,7 @@ class RecoveryEmailService { return $user->getEMailAddress(); } - private function validateBasicRecoveryEmailRules(string $recoveryEmail, string $username, string $email, IL10N $l): void { + private function enforceBasicRecoveryEmailRules(string $recoveryEmail, string $username, string $email, IL10N $l): void { if (empty($recoveryEmail)) { return; } @@ -174,7 +174,7 @@ class RecoveryEmailService { } } - private function verifyEmailWithApi(string $recoveryEmail, string $username, string $apiKey): void { + private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey): void { $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); $response = file_get_contents($url); $data = json_decode($response, true); -- GitLab From 247d4be1fc3c5a20837887ebc8dbce1b9a45c263 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 13:55:19 +0530 Subject: [PATCH 28/74] applied suggestion --- lib/Command/UpdateBlacklistedDomains.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Command/UpdateBlacklistedDomains.php b/lib/Command/UpdateBlacklistedDomains.php index b0bbed8..b677f68 100644 --- a/lib/Command/UpdateBlacklistedDomains.php +++ b/lib/Command/UpdateBlacklistedDomains.php @@ -5,20 +5,20 @@ declare(strict_types=1); namespace OCA\EmailRecovery\Command; use OCA\EmailRecovery\AppInfo\Application; -use OCA\EmailRecovery\Service\BlackListService; +use OCA\EmailRecovery\Service\DomainService; use OCP\ILogger; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class UpdateBlacklistedDomains extends Command { - private BlackListService $blackListService; + private DomainService $domainService; private ILogger $logger; - public function __construct(BlackListService $blackListService, ILogger $logger) { + public function __construct(DomainService $domainService, ILogger $logger) { parent::__construct(); - $this->blackListService = $blackListService; + $this->domainService = $domainService; $this->logger = $logger; } @@ -28,7 +28,7 @@ class UpdateBlacklistedDomains extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { try { - $this->blackListService->updateBlacklistedDomains(); + $this->domainService->updateBlacklistedDomains(); $output->writeln('Updated blacklisted domains for creation.'); } catch (\Throwable $th) { $this->logger->error('Error while updating blacklisted domains. ' . $th->getMessage()); -- GitLab From 82858f0c99cae874e8603721b862bc5dda0bcf5d Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 14:10:26 +0530 Subject: [PATCH 29/74] change to DomainService --- lib/Command/CreatePopularDomain.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 76865cb..60b81c7 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace OCA\EmailRecovery\Command; use OCA\EmailRecovery\AppInfo\Application; -use OCA\EmailRecovery\Service\PopularDomainService; +use OCA\EmailRecovery\Service\DomainService; use OCP\ILogger; use OCP\Files\IAppData; use Symfony\Component\Console\Command\Command; @@ -13,12 +13,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class CreatePopularDomain extends Command { - private PopularDomainService $domainService; + private DomainService $domainService; private ILogger $logger; private IAppData $appData; private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; - public function __construct(PopularDomainService $domainService, ILogger $logger, IAppData $appData) { + public function __construct(DomainService $domainService, ILogger $logger, IAppData $appData) { parent::__construct(); $this->domainService = $domainService; $this->logger = $logger; -- GitLab From 3717afb8a2e1485900c348ab5bb72ba740aae137 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 14:32:07 +0530 Subject: [PATCH 30/74] added fetchPopularDomain --- lib/Service/DomainService.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 324ff79..defa588 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -156,4 +156,29 @@ class DomainService { throw new \RuntimeException('Failed to update popular domains file.', 0, $e); } } + + /** + * Fetch popular email domains from an external source. + * + * @return array List of popular domains. + */ + public function fetchPopularDomains(): array { + $url = self::POPULAR_DOMAINS_URL; + try { + $response = $this->httpClient->request('GET', $url); + $data = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Failed to decode JSON: ' . json_last_error_msg()); + } + + return $data; + } catch (RequestException $e) { + $this->logger->error('HTTP request failed: ' . $e->getMessage()); + throw new \RuntimeException('Failed to fetch popular domains.'); + } catch (\Throwable $th) { + $this->logger->error('Unexpected error: ' . $th->getMessage()); + throw $th; + } + } } -- GitLab From 4586b1e9155fd78d415252c82491089cef13b9ce Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Thu, 16 Jan 2025 15:25:27 +0530 Subject: [PATCH 31/74] removed method --- lib/Command/CreatePopularDomain.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 60b81c7..43bb5b2 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -43,20 +43,4 @@ class CreatePopularDomain extends Command { return Command::SUCCESS; } - - - private function getPopularDomainsFile() { - try { - $currentFolder = $this->appData->getFolder('/'); - } catch (NotFoundException $e) { - $currentFolder = $this->appData->newFolder('/'); - } - - $filename = self::POPULAR_DOMAINS_FILE_NAME; - if ($currentFolder->fileExists($filename)) { - return $currentFolder->getFile($filename); - } - - return $currentFolder->newFile($filename); - } } -- GitLab From 01b450bf2b3693fa03199fd721e388c753e03486 Mon Sep 17 00:00:00 2001 From: AVINASH GUSAIN Date: Fri, 17 Jan 2025 12:41:10 +0000 Subject: [PATCH 32/74] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Akhil --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index c0edbf0..5a19455 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -62,7 +62,7 @@ class RecoveryEmailService { $this->domainService = $domainService; $this->l = $l; // Initialize Redis cache directly - $this->cache = \OC::$server->getMemCacheFactory()->createDistributed(); + $this->cache = $cacheFactory->createDistributed(self::CACHE_KEY); $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { -- GitLab From 3030fc3a37d4d7eeedb7d198c85a607c628fa7d2 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 18:27:39 +0530 Subject: [PATCH 33/74] applied suggestions --- lib/Service/DomainService.php | 2 +- lib/Service/RecoveryEmailService.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index defa588..d741133 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -162,7 +162,7 @@ class DomainService { * * @return array List of popular domains. */ - public function fetchPopularDomains(): array { + private function fetchPopularDomains(): array { $url = self::POPULAR_DOMAINS_URL; try { $response = $this->httpClient->request('GET', $url); diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 5a19455..388e490 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -120,7 +120,12 @@ class RecoveryEmailService { $l = $this->l10nFactory->get($this->appName, $language); $this->enforceBasicRecoveryEmailRules($recoveryEmail, $username, $email, $l); - $apiKey = getenv('VERIFYMAIL_API_KEY'); + $apiKey = $this->config->getSystemValue('verify_mail_api_key', ''); + + if (empty($apiKey)) { + $this->logger->error('VerifyMail API Key is not configured.'); + throw new \RuntimeException('VerifyMail API Key is missing.'); + } // Check if it's a popular domain and not custom blacklist, then verify the email if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail)) { -- GitLab From 95e4a08f59e33246534b55002c5d43683cc53d00 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 19:11:13 +0530 Subject: [PATCH 34/74] applied suggestions --- lib/Service/RecoveryEmailService.php | 34 +++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 388e490..ae4fd60 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -387,28 +387,36 @@ class RecoveryEmailService { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } - private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 5): void { - $now = microtime(true); - - for ($retry = 0; $retry < $maxRetries; $retry++) { + private function ensureRealTimeRateLimit( + string $key, + int $rateLimit, + float $timeWindow = 1, // Time window for rate limiting in seconds + float $maxTimeThreshold = 10, // Maximum total retry time in seconds + float $retryInterval = 0.1 // Retry interval in seconds (default 100ms) + ): void { + $startTime = microtime(true); + + while ((microtime(true) - $startTime) < $maxTimeThreshold) { + $now = microtime(true); $requests = $this->cache->get($key) ?? []; - // Remove old requests (keep only those within a 1-second window) - $requests = array_filter($requests, function ($timestamp) use ($now) { - return ($now - $timestamp) <= 1; + // Remove requests older than the time window + $requests = array_filter($requests, function ($timestamp) use ($now, $timeWindow) { + return ($now - $timestamp) <= $timeWindow; }); if (count($requests) < $rateLimit) { - // Under the rate limit, add the current request timestamp and proceed + // Add the current request timestamp and proceed $requests[] = $now; - $this->cache->set($key, $requests, 2); // Set expiry of 2 seconds for cached timestamps - return; + $this->cache->set($key, $requests, $timeWindow + 1); // Expiry slightly beyond the time window + return; // Request is allowed } - // If rate limit exceeded, wait a short time and retry - usleep(100000); // Wait 100ms before retrying (adjust as needed) + // Wait before retrying + usleep((int) ($retryInterval * 1_000_000)); // Convert seconds to microseconds } - throw new \Exception("Rate limit exceeded. Please try again later."); + // If the time threshold is exceeded, throw an exception + throw new \Exception("Rate limit exceeded. Please try again after some time."); } } -- GitLab From c6148819b04334db7299d7826de364c61dc7e196 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 19:13:58 +0530 Subject: [PATCH 35/74] applied suggestions --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index ae4fd60..82a8cba 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -392,7 +392,7 @@ class RecoveryEmailService { int $rateLimit, float $timeWindow = 1, // Time window for rate limiting in seconds float $maxTimeThreshold = 10, // Maximum total retry time in seconds - float $retryInterval = 0.1 // Retry interval in seconds (default 100ms) + float $retryInterval = 0.2 // Retry interval in seconds (default 200ms) ): void { $startTime = microtime(true); -- GitLab From efdd10019ddd88c74f65336b60cf6299f7d3564f Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 20:34:04 +0530 Subject: [PATCH 36/74] added retries login for 429 -5 times and also modified code for ensureRealTimeRateLimit --- lib/Service/RecoveryEmailService.php | 155 +++++++++++++++++---------- 1 file changed, 101 insertions(+), 54 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 82a8cba..2ffa5ca 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -178,45 +178,106 @@ class RecoveryEmailService { throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } } + + private function retryApiCall(callable $callback, int $maxRetries = 5, int $initialInterval = 1000): void { + $retryInterval = $initialInterval; + $retries = 0; + + while ($retries < $maxRetries) { + try { + // Execute the API call + $callback(); + return; // Exit if the API call succeeds + } catch (\Exception $e) { + // Check for rate-limiting (HTTP 429) + if ($e instanceof \RuntimeException && $e->getCode() === 429) { + $this->logger->warning("Received 429 status code, retrying in $retryInterval ms..."); + usleep($retryInterval * 1000); // Convert to microseconds + $retryInterval *= 2; // Exponential backoff + $retries++; + continue; // Retry on 429 error + } + + // For other exceptions (non-429) + $this->logger->warning("API call failed on attempt $retries/$maxRetries. Error: " . $e->getMessage()); + if ($retries >= $maxRetries) { + throw new \RuntimeException("API call failed after $maxRetries retries.", 0, $e); + } + + // Retry logic for non-429 failures (network, timeouts, etc.) + $retries++; + usleep($retryInterval * 1000); // Exponential backoff for non-429 failures + $retryInterval *= 2; // Exponential backoff + } + } + } - private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey): void { - $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); - $response = file_get_contents($url); - $data = json_decode($response, true); - if ($data['disposable'] ?? false) { - $this->logger->info("User ID $username's requested recovery email address is disposable."); - throw new BlacklistedEmailException("The email address is disposable. Please provide another recovery address."); - } - if (!$data['deliverable_email'] ?? true) { - $this->logger->info("User ID $username's requested recovery email address is not deliverable."); - throw new BlacklistedEmailException("The email address is not deliverable. Please provide another recovery address."); - } + private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey): void { + $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); - $this->logger->info("User ID $username's requested recovery email address is valid and not disposable."); + $this->retryApiCall(function () use ($url, $username) { + try { + // Make the API request + $response = $this->httpClient->request('GET', $url, [ + 'timeout' => 5, // Timeout for the API call + ]); + + // Process response, handle errors (e.g., disposable email, non-deliverable email) + $data = json_decode($response->getBody()->getContents(), true); + + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is disposable."); + throw new BlacklistedEmailException("The email address is disposable. Please provide another recovery address."); + } + + if (!$data['deliverable_email'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address is not deliverable."); + throw new BlacklistedEmailException("The email address is not deliverable. Please provide another recovery address."); + } + } catch (\Exception $e) { + // Optionally handle specific exceptions if needed here (e.g., timeouts, network errors) + $this->logger->error("Error while validating email for user $username: " . $e->getMessage()); + throw $e; // Re-throw if necessary + } + }); } + + private function verifyDomainWithApi(string $domain, string $username, string $apiKey): void { $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); - $response = file_get_contents($url); - $data = json_decode($response, true); - if ($data['disposable'] ?? false) { - $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); - $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); - throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); - } + $this->retryApiCall(function () use ($url, $username, $domain) { + // Send the API request + $response = file_get_contents($url); + $data = json_decode($response, true); - if (!$data['mx'] ?? true) { - $this->logger->info("User ID $username's requested recovery email address domain is not valid."); - $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); - throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); - } + // Check if the data is properly structured + if (!$data || !is_array($data)) { + throw new \RuntimeException("Invalid response received while verifying domain: " . $response); + } + + // Handle response data + if ($data['disposable'] ?? false) { + $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); + $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); + throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); + } + + if (!$data['mx'] ?? true) { + $this->logger->info("User ID $username's requested recovery email address domain is not valid."); + $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); + throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); + } - $this->logger->info("User ID $username's requested recovery email address domain is valid."); + $this->logger->info("User ID $username's requested recovery email address domain is valid."); + }); } + + public function isRecoveryEmailDomainDisallowed(string $recoveryEmail): bool { $recoveryEmail = strtolower($recoveryEmail); $emailParts = explode('@', $recoveryEmail); @@ -387,36 +448,22 @@ class RecoveryEmailService { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } - private function ensureRealTimeRateLimit( - string $key, - int $rateLimit, - float $timeWindow = 1, // Time window for rate limiting in seconds - float $maxTimeThreshold = 10, // Maximum total retry time in seconds - float $retryInterval = 0.2 // Retry interval in seconds (default 200ms) - ): void { - $startTime = microtime(true); - - while ((microtime(true) - $startTime) < $maxTimeThreshold) { - $now = microtime(true); - $requests = $this->cache->get($key) ?? []; - - // Remove requests older than the time window - $requests = array_filter($requests, function ($timestamp) use ($now, $timeWindow) { - return ($now - $timestamp) <= $timeWindow; - }); - - if (count($requests) < $rateLimit) { - // Add the current request timestamp and proceed - $requests[] = $now; - $this->cache->set($key, $requests, $timeWindow + 1); // Expiry slightly beyond the time window - return; // Request is allowed - } + private function ensureRealTimeRateLimit(string $key, int $rateLimit): void { + $now = microtime(true); + $requests = $this->cache->get($key) ?? []; + + // Filter out requests that are older than 1 second (sliding window) + $requests = array_filter($requests, function ($timestamp) use ($now) { + return ($now - $timestamp) <= 1; // 1-second sliding window + }); - // Wait before retrying - usleep((int) ($retryInterval * 1_000_000)); // Convert seconds to microseconds + // Check if rate limit has been exceeded + if (count($requests) >= $rateLimit) { + throw new \RuntimeException('Rate limit exceeded.'); } - // If the time threshold is exceeded, throw an exception - throw new \Exception("Rate limit exceeded. Please try again after some time."); + // Add the current timestamp of the received request + $requests[] = $now; + $this->cache->set($key, $requests, 2); // Slightly longer expiration } } -- GitLab From 37da5b7d6b856b9491edb95aa756c5753ab9fe64 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 20:36:23 +0530 Subject: [PATCH 37/74] added retries login for 429 -5 times and also modified code for ensureRealTimeRateLimit --- lib/Service/RecoveryEmailService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 2ffa5ca..43d9d51 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -123,8 +123,8 @@ class RecoveryEmailService { $apiKey = $this->config->getSystemValue('verify_mail_api_key', ''); if (empty($apiKey)) { - $this->logger->error('VerifyMail API Key is not configured.'); - throw new \RuntimeException('VerifyMail API Key is missing.'); + $this->logger->info('VerifyMail API Key is not configured.'); + throw new \RuntimeException($l->t('Error! User email address cannot be saved as recovery email address!')); } // Check if it's a popular domain and not custom blacklist, then verify the email -- GitLab From 32ada8a80f5a5edf1eee7e5f960acbb95800da6c Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 20:44:05 +0530 Subject: [PATCH 38/74] no exception from queuing --- lib/Service/RecoveryEmailService.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 43d9d51..71ac3d3 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -452,18 +452,23 @@ class RecoveryEmailService { $now = microtime(true); $requests = $this->cache->get($key) ?? []; - // Filter out requests that are older than 1 second (sliding window) + // Filter out requests older than 1 second (sliding window) $requests = array_filter($requests, function ($timestamp) use ($now) { - return ($now - $timestamp) <= 1; // 1-second sliding window + return ($now - $timestamp) <= 1; }); - // Check if rate limit has been exceeded if (count($requests) >= $rateLimit) { - throw new \RuntimeException('Rate limit exceeded.'); + // Calculate delay to maintain rate limit + $oldestRequest = min($requests); + $delay = 1 - ($now - $oldestRequest); + + if ($delay > 0) { + usleep((int)($delay * 1_000_000)); // Convert to microseconds and delay + } } - // Add the current timestamp of the received request - $requests[] = $now; - $this->cache->set($key, $requests, 2); // Slightly longer expiration + // Add the current request timestamp + $requests[] = microtime(true); + $this->cache->set($key, $requests, 2); } } -- GitLab From eb3c4bae4c50ebb840098694874a0ba050a88dac Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 21:06:29 +0530 Subject: [PATCH 39/74] cachefactory added --- lib/Service/RecoveryEmailService.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 71ac3d3..807c460 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -24,6 +24,7 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Util; +use OCP\ICacheFactory; class RecoveryEmailService { private ILogger $logger; @@ -35,6 +36,7 @@ class RecoveryEmailService { private IURLGenerator $urlGenerator; private Defaults $themingDefaults; private IVerificationToken $verificationToken; + private ICacheFactory $cacheFactory; private CurlService $curl; private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes @@ -47,7 +49,7 @@ class RecoveryEmailService { private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -61,8 +63,8 @@ class RecoveryEmailService { $this->curl = $curlService; $this->domainService = $domainService; $this->l = $l; - // Initialize Redis cache directly - $this->cache = $cacheFactory->createDistributed(self::CACHE_KEY); + $this->cacheFactory = $cacheFactory; // Initialize the cache factory + $this->cache = $this->cacheFactory->createDistributed(self::CACHE_KEY); // Initialize the cache $commonServiceURL = $this->config->getSystemValue('common_services_url', ''); if (!empty($commonServiceURL)) { -- GitLab From 4d2cdb9f9d8a000d0fac7cc430f83bc5de20ca49 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Fri, 17 Jan 2025 21:40:19 +0530 Subject: [PATCH 40/74] key added --- lib/Service/RecoveryEmailService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 807c460..17f388e 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -41,6 +41,7 @@ class RecoveryEmailService { private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; + private const CACHE_KEY = 'recovery_email_rate_limit'; private const VERIFYMAIL_API_URL = 'https://verifymail.io/api/%s?key=%s'; private const RATE_LIMIT_EMAIL = 'verifymail_email_ratelimit'; private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; -- GitLab From 0ded0cd28477792ccf5858cf2c59bf97b562e5da Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 01:26:44 +0530 Subject: [PATCH 41/74] error msg on 200 code --- lib/Service/RecoveryEmailService.php | 48 ++++++++++++++++++---------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 17f388e..030a889 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -182,17 +182,24 @@ class RecoveryEmailService { } } - private function retryApiCall(callable $callback, int $maxRetries = 5, int $initialInterval = 1000): void { - $retryInterval = $initialInterval; + private function retryApiCall(callable $callback, int $maxRetries = 5, int $initialInterval = 1000) { + $retryInterval = $initialInterval; // Initial retry interval in milliseconds $retries = 0; while ($retries < $maxRetries) { try { - // Execute the API call - $callback(); - return; // Exit if the API call succeeds + // Execute the API call and return if successful + $result = $callback(); + + // Assuming the callback returns the response on success + if ($result) { + return $result; // Exit and return the successful response + } + + // Log a warning if the result indicates a failure + $this->logger->warning("API call returned a failure response: " . print_r($result, true)); } catch (\Exception $e) { - // Check for rate-limiting (HTTP 429) + // Handle rate-limiting (HTTP 429) if ($e instanceof \RuntimeException && $e->getCode() === 429) { $this->logger->warning("Received 429 status code, retrying in $retryInterval ms..."); usleep($retryInterval * 1000); // Convert to microseconds @@ -201,22 +208,26 @@ class RecoveryEmailService { continue; // Retry on 429 error } - // For other exceptions (non-429) + // Log non-429 errors and retry $this->logger->warning("API call failed on attempt $retries/$maxRetries. Error: " . $e->getMessage()); if ($retries >= $maxRetries) { - throw new \RuntimeException("API call failed after $maxRetries retries.", 0, $e); + throw new \RuntimeException("API call failed after $maxRetries retries. Error: " . $e->getMessage(), 0, $e); } - - // Retry logic for non-429 failures (network, timeouts, etc.) - $retries++; - usleep($retryInterval * 1000); // Exponential backoff for non-429 failures - $retryInterval *= 2; // Exponential backoff } + + // Retry logic for all errors + $retries++; + usleep($retryInterval * 1000); // Delay between retries + $retryInterval *= 2; // Exponential backoff } + + // Throw an exception if retries are exhausted + throw new \RuntimeException("API call failed after maximum retries ($maxRetries)."); } + private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey): void { $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); @@ -253,9 +264,14 @@ class RecoveryEmailService { $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); $this->retryApiCall(function () use ($url, $username, $domain) { - // Send the API request - $response = file_get_contents($url); - $data = json_decode($response, true); + // Make the API request + $response = $this->httpClient->request('GET', $url, [ + 'timeout' => 5, // Timeout for the API call + ]); + + // Process response, handle errors (e.g., disposable email, non-deliverable email) + $data = json_decode($response->getBody()->getContents(), true); + // Check if the data is properly structured if (!$data || !is_array($data)) { -- GitLab From 91bf017226bfe268d8136a6255fb5928b30e08e8 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 01:44:44 +0530 Subject: [PATCH 42/74] error msg on 200 code --- lib/Service/RecoveryEmailService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 030a889..5acd350 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -25,6 +25,7 @@ use OCP\Mail\IMailer; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Util; use OCP\ICacheFactory; +use OCP\Http\Client\IClient; class RecoveryEmailService { private ILogger $logger; @@ -38,6 +39,7 @@ class RecoveryEmailService { private IVerificationToken $verificationToken; private ICacheFactory $cacheFactory; private CurlService $curl; + private IClient $httpClient; private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; @@ -50,7 +52,7 @@ class RecoveryEmailService { private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory, IClient $httpClient) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -63,6 +65,7 @@ class RecoveryEmailService { $this->verificationToken = $verificationToken; $this->curl = $curlService; $this->domainService = $domainService; + $this->httpClient = $httpClient; $this->l = $l; $this->cacheFactory = $cacheFactory; // Initialize the cache factory $this->cache = $this->cacheFactory->createDistributed(self::CACHE_KEY); // Initialize the cache -- GitLab From a6573ce29013b289c6786ab9404270b516aa986a Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 01:57:37 +0530 Subject: [PATCH 43/74] used httpClientService --- lib/Service/RecoveryEmailService.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 5acd350..6f0cf68 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -25,7 +25,7 @@ use OCP\Mail\IMailer; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Util; use OCP\ICacheFactory; -use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; class RecoveryEmailService { private ILogger $logger; @@ -39,7 +39,7 @@ class RecoveryEmailService { private IVerificationToken $verificationToken; private ICacheFactory $cacheFactory; private CurlService $curl; - private IClient $httpClient; + private IClientService $httpClientService; private array $apiConfig; protected const TOKEN_LIFETIME = 60 * 30; // 30 minutes private const ATTEMPT_KEY = "recovery_email_attempts"; @@ -52,7 +52,7 @@ class RecoveryEmailService { private IL10N $l; private ISession $session; - public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory, IClient $httpClient) { + public function __construct(string $appName, ILogger $logger, IConfig $config, ISession $session, IUserManager $userManager, IMailer $mailer, IFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $themingDefaults, IVerificationToken $verificationToken, CurlService $curlService, DomainService $domainService, IL10N $l, ICacheFactory $cacheFactory, IClientService $httpClientService) { $this->logger = $logger; $this->config = $config; $this->appName = $appName; @@ -65,7 +65,7 @@ class RecoveryEmailService { $this->verificationToken = $verificationToken; $this->curl = $curlService; $this->domainService = $domainService; - $this->httpClient = $httpClient; + $this->httpClientService = $httpClientService; $this->l = $l; $this->cacheFactory = $cacheFactory; // Initialize the cache factory $this->cache = $this->cacheFactory->createDistributed(self::CACHE_KEY); // Initialize the cache @@ -237,7 +237,7 @@ class RecoveryEmailService { $this->retryApiCall(function () use ($url, $username) { try { // Make the API request - $response = $this->httpClient->request('GET', $url, [ + $response = $this->httpClientService->request('GET', $url, [ 'timeout' => 5, // Timeout for the API call ]); @@ -268,7 +268,7 @@ class RecoveryEmailService { $this->retryApiCall(function () use ($url, $username, $domain) { // Make the API request - $response = $this->httpClient->request('GET', $url, [ + $response = $this->httpClientService->request('GET', $url, [ 'timeout' => 5, // Timeout for the API call ]); -- GitLab From e296439f3dc90e5b234d35aedeb72797323d1b41 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 02:10:16 +0530 Subject: [PATCH 44/74] used httpClientService --- lib/Service/RecoveryEmailService.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 6f0cf68..9bd7f48 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -236,8 +236,9 @@ class RecoveryEmailService { $this->retryApiCall(function () use ($url, $username) { try { + $httpClient = $this->httpClientService->newClient(); // Make the API request - $response = $this->httpClientService->request('GET', $url, [ + $response = $httpClient->request('GET', $url, [ 'timeout' => 5, // Timeout for the API call ]); @@ -267,8 +268,9 @@ class RecoveryEmailService { $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); $this->retryApiCall(function () use ($url, $username, $domain) { + $httpClient = $this->httpClientService->newClient(); // Make the API request - $response = $this->httpClientService->request('GET', $url, [ + $response = $httpClient->request('GET', $url, [ 'timeout' => 5, // Timeout for the API call ]); -- GitLab From 965072544fdc1b0b05a18dc3cfc2d6b34053a077 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 17:06:31 +0530 Subject: [PATCH 45/74] use get method --- lib/Service/RecoveryEmailService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 9bd7f48..2ec617b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -238,7 +238,7 @@ class RecoveryEmailService { try { $httpClient = $this->httpClientService->newClient(); // Make the API request - $response = $httpClient->request('GET', $url, [ + $response = $httpClient->get($url, [ 'timeout' => 5, // Timeout for the API call ]); @@ -270,7 +270,7 @@ class RecoveryEmailService { $this->retryApiCall(function () use ($url, $username, $domain) { $httpClient = $this->httpClientService->newClient(); // Make the API request - $response = $httpClient->request('GET', $url, [ + $response = $httpClient->get($url, [ 'timeout' => 5, // Timeout for the API call ]); -- GitLab From 689dfdb3fcd6da0f61d3c0dd9c2bf4cccd939207 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 17:24:41 +0530 Subject: [PATCH 46/74] use getBody to get response --- lib/Service/RecoveryEmailService.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 2ec617b..3a9494e 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -243,7 +243,8 @@ class RecoveryEmailService { ]); // Process response, handle errors (e.g., disposable email, non-deliverable email) - $data = json_decode($response->getBody()->getContents(), true); + $responseBody = $response->getBody(); // Get the response body as a string + $data = json_decode($responseBody, true); if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is disposable."); @@ -275,7 +276,8 @@ class RecoveryEmailService { ]); // Process response, handle errors (e.g., disposable email, non-deliverable email) - $data = json_decode($response->getBody()->getContents(), true); + $responseBody = $response->getBody(); // Get the response body as a string + $data = json_decode($responseBody, true); // Check if the data is properly structured -- GitLab From f5d050f0c7cbb0b5d2c4387a42ca7c75e11e3809 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Sat, 18 Jan 2025 17:44:32 +0530 Subject: [PATCH 47/74] retry handling --- lib/Service/RecoveryEmailService.php | 44 +++++++++++----------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 3a9494e..50cf1ac 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -185,52 +185,42 @@ class RecoveryEmailService { } } - private function retryApiCall(callable $callback, int $maxRetries = 5, int $initialInterval = 1000) { + private function retryApiCall(callable $callback, int $maxRetries = 5, int $initialInterval = 1000): void { $retryInterval = $initialInterval; // Initial retry interval in milliseconds $retries = 0; while ($retries < $maxRetries) { try { - // Execute the API call and return if successful + // Execute the API call $result = $callback(); - // Assuming the callback returns the response on success - if ($result) { - return $result; // Exit and return the successful response - } - - // Log a warning if the result indicates a failure - $this->logger->warning("API call returned a failure response: " . print_r($result, true)); + // If successful, return immediately + return; } catch (\Exception $e) { - // Handle rate-limiting (HTTP 429) + // Check for rate-limiting (HTTP 429) if ($e instanceof \RuntimeException && $e->getCode() === 429) { + $retries++; + + if ($retries >= $maxRetries) { + throw new \RuntimeException("API call failed after $maxRetries retries. Error: " . $e->getMessage(), 0, $e); + } + $this->logger->warning("Received 429 status code, retrying in $retryInterval ms..."); usleep($retryInterval * 1000); // Convert to microseconds $retryInterval *= 2; // Exponential backoff - $retries++; - continue; // Retry on 429 error + continue; // Retry only on 429 errors } - // Log non-429 errors and retry - $this->logger->warning("API call failed on attempt $retries/$maxRetries. Error: " . $e->getMessage()); - if ($retries >= $maxRetries) { - throw new \RuntimeException("API call failed after $maxRetries retries. Error: " . $e->getMessage(), 0, $e); - } + // For other exceptions, log and rethrow immediately without retrying + $this->logger->error("API call failed on the first attempt. Error: " . $e->getMessage()); + throw $e; } - - // Retry logic for all errors - $retries++; - usleep($retryInterval * 1000); // Delay between retries - $retryInterval *= 2; // Exponential backoff } - // Throw an exception if retries are exhausted - throw new \RuntimeException("API call failed after maximum retries ($maxRetries)."); + // Shouldn't reach here since retries are handled above + throw new \RuntimeException("API call failed unexpectedly after maximum retries."); } - - - private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey): void { $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); -- GitLab From 82a2b838122c6620b4d2551c455ae920161158bb Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 10:37:40 +0530 Subject: [PATCH 48/74] applied trans --- l10n/de.js | 5 ++++- l10n/de.json | 5 ++++- l10n/de_DE.js | 5 ++++- l10n/de_DE.json | 5 ++++- l10n/en.js | 5 ++++- l10n/en.json | 5 ++++- l10n/es.js | 5 ++++- l10n/es.json | 5 ++++- l10n/fr.js | 5 ++++- l10n/fr.json | 5 ++++- l10n/it.js | 5 ++++- l10n/it.json | 5 ++++- lib/Service/RecoveryEmailService.php | 6 +++--- 13 files changed, 51 insertions(+), 15 deletions(-) diff --git a/l10n/de.js b/l10n/de.js index f3d97fc..7463cfc 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/de.json b/l10n/de.json index b96b52d..5fd1977 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von dir zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Du wirst sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/de_DE.js b/l10n/de_DE.js index 8d14855..b228711 100644 --- a/l10n/de_DE.js +++ b/l10n/de_DE.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/de_DE.json b/l10n/de_DE.json index f914651..45ba9a5 100644 --- a/l10n/de_DE.json +++ b/l10n/de_DE.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Bitte geben Sie eine Wiederherstellungs-E-Mail-Adresse an.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Die Domäne dieser E-Mailadresse ist auf der Sperrliste. Bitte geben Sie eine andere E-Mailadresse an.", "Too many verification emails.": "Zu viele Bestätigungs-E-Mails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Diese Version von Murena Workspace erlaubt nur einen minimalen Zugriff. Das bedeutet, dass einige von Ihnen zuvor festgelegten Konfigurationen (z. B. zusätzliche E-Mail-Konten) sowie einige Funktionen (z. B. Dateien, PGP) möglicherweise nicht vorhanden sind. Sie werden sie wiederfinden, wenn alles wieder normal läuft.", + "The email could not be verified. Please try again later.": "Die E-Mail konnte nicht verifiziert werden. Bitte versuchen Sie es später noch einmal.", + "The email address is disposable. Please provide another recovery address." : "Die E-Mail-Adresse ist eine Wegwerfadresse. Bitte geben Sie eine andere Wiederherstellungsadresse an.", + "The email address is not deliverable. Please provide another recovery address.": "Die E-Mail Adresse ist nicht zustellbar. Bitte geben Sie eine andere Wiederherstellungsadresse an." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/en.js b/l10n/en.js index f8174ea..3ef0c60 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -31,6 +31,9 @@ OC.L10N.register( "Please set a recovery email address.": "Please set a recovery email address.", "The domain of this email address is blacklisted. Please provide another recovery address.": "The domain of this email address is blacklisted. Please provide another recovery address.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.", + "The email could not be verified. Please try again later.": "The email could not be verified. Please try again later.", + "The email address is disposable. Please provide another recovery address." : "The email address is disposable. Please provide another recovery address.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/en.json b/l10n/en.json index 239b92c..6ca785f 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -32,7 +32,10 @@ "Please set a recovery email address.": "Please set a recovery email address.", "The domain of this email address is blacklisted. Please provide another recovery address.": "The domain of this email address is blacklisted. Please provide another recovery address.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.", + "The email could not be verified. Please try again later.": "The email could not be verified. Please try again later.", + "The email address is disposable. Please provide another recovery address." : "The email address is disposable. Please provide another recovery address.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "pluralForm": "nplurals=2; plural=(n != 1);" } \ No newline at end of file diff --git a/l10n/es.js b/l10n/es.js index 45eccc5..66500df 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Por favor configura un correo electrónico para recuperación.", "The domain of this email address is blacklisted. Please provide another recovery address.": "El dominio de esta dirección de correo electrónico está en lista negra. Por favor, proporciona otra dirección de recuperación.", "Too many verification emails.": "Demasiados correos electrónicos de verificación.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "EEsta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "EEsta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad.", + "The email could not be verified. Please try again later.": "No se ha podido verificar el correo electrónico. Inténtelo de nuevo más tarde.", + "The email address is disposable. Please provide another recovery address." : "La dirección de correo electrónico es desechable. Por favor, proporcione otra dirección de recuperación.", + "The email address is not deliverable. Please provide another recovery address.": "La dirección de correo electrónico no se puede entregar. Por favor, proporcione otra dirección de recuperación." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/es.json b/l10n/es.json index b078c42..74af0bc 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Por favor configura un correo electrónico para recuperación.", "The domain of this email address is blacklisted. Please provide another recovery address.": "El dominio de esta dirección de correo electrónico está en lista negra. Por favor, proporciona otra dirección de recuperación.", "Too many verification emails.": "Demasiados correos electrónicos de verificación.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Esta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Esta versión de Murena Workspace sólo te permite un acceso mínimo. Esto significa que algunas configuraciones que hayas hecho anteriormente (por ejemplo, cuentas de correo adicionales) así como algunas funcionalidades (por ejemplo, Archivos, PGP) no estarán disponibles. Recuperarás todas tus funciones cuando todo vuelva a la normalidad.", + "The email could not be verified. Please try again later.": "No se ha podido verificar el correo electrónico. Inténtelo de nuevo más tarde.", + "The email address is disposable. Please provide another recovery address." : "La dirección de correo electrónico es desechable. Por favor, proporcione otra dirección de recuperación.", + "The email address is not deliverable. Please provide another recovery address.": "La dirección de correo electrónico no se puede entregar. Por favor, proporcione otra dirección de recuperación." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/l10n/fr.js b/l10n/fr.js index 7fa0cc0..90ad988 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Merci d'ajouter une adresse e-mail de récupération.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Le domain de cette adresse e-mail est sur liste noire. Merci de bien vouloir fournir une autre adresse de récupération.", "Too many verification emails.": "Trop de courriels de vérification.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale.", + "The email could not be verified. Please try again later.": "L'e-mail n'a pas pu être vérifié. Veuillez réessayer plus tard.", + "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "nplurals=2; plural=n > 1;"); diff --git a/l10n/fr.json b/l10n/fr.json index d660d41..3602a0d 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Merci d'ajouter une adresse e-mail de récupération.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Le domain de cette adresse e-mail est sur liste noire. Merci de bien vouloir fournir une autre adresse de récupération.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale.", + "The email could not be verified. Please try again later.": "L'e-mail n'a pas pu être vérifié. Veuillez réessayer plus tard.", + "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." },"pluralForm" :"nplurals=2; plural=n > 1;" } \ No newline at end of file diff --git a/l10n/it.js b/l10n/it.js index af51b41..53f5fa6 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -34,6 +34,9 @@ OC.L10N.register( "Please set a recovery email address.": "Imposta un indirizzo e-mail di recovery.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Il dominio cui appartiene questo indirizzo e-mail è contenuto in una black list. Inserisci un indirizzo di recovery differente.", "Too many verification emails.": "Troppe e-mail di verifica.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità.", + "The email could not be verified. Please try again later.": "Non è stato possibile verificare l'e-mail. Si prega di riprovare più tardi.", + "The email address is disposable. Please provide another recovery address." : "L'indirizzo e-mail è monouso. Si prega di fornire un altro indirizzo di recupero.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." }, "nplurals=2; plural=n != 1;"); diff --git a/l10n/it.json b/l10n/it.json index 84fd9e6..5e9165f 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -32,6 +32,9 @@ "Please set a recovery email address.": "Imposta un indirizzo e-mail di recovery.", "The domain of this email address is blacklisted. Please provide another recovery address.": "Il dominio cui appartiene questo indirizzo e-mail è contenuto in una black list. Inserisci un indirizzo di recovery differente.", "Too many verification emails.": "Too many verification emails.", - "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità." + "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Questa versione di Murena Workspace consente l'accesso con alcune restrizioni. Ciò significa che alcune configurazioni impostate in precedenza (ad esempio, account di posta aggiuntivi) ed alcune funzionalità (ad esempio File, PGP) potrebbero non essere disponibili. Verranno riattivate quando tutto tornerà alla normalità.", + "The email could not be verified. Please try again later.": "Non è stato possibile verificare l'e-mail. Si prega di riprovare più tardi.", + "The email address is disposable. Please provide another recovery address." : "L'indirizzo e-mail è monouso. Si prega di fornire un altro indirizzo di recupero.", + "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." },"pluralForm" :"nplurals=2; plural=n != 1;" } \ No newline at end of file diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 50cf1ac..e30792b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -202,7 +202,7 @@ class RecoveryEmailService { $retries++; if ($retries >= $maxRetries) { - throw new \RuntimeException("API call failed after $maxRetries retries. Error: " . $e->getMessage(), 0, $e); + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); } $this->logger->warning("Received 429 status code, retrying in $retryInterval ms..."); @@ -238,12 +238,12 @@ class RecoveryEmailService { if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is disposable."); - throw new BlacklistedEmailException("The email address is disposable. Please provide another recovery address."); + throw new BlacklistedEmailException($l->t('"The email address is disposable. Please provide another recovery address.')); } if (!$data['deliverable_email'] ?? true) { $this->logger->info("User ID $username's requested recovery email address is not deliverable."); - throw new BlacklistedEmailException("The email address is not deliverable. Please provide another recovery address."); + throw new BlacklistedEmailException($l->t('The email address is not deliverable. Please provide another recovery address.')); } } catch (\Exception $e) { // Optionally handle specific exceptions if needed here (e.g., timeouts, network errors) -- GitLab From fe6ad1b75d4efb8c59067806fcd8949fbe904f5c Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 10:41:39 +0530 Subject: [PATCH 49/74] delay remove _ --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index e30792b..18c2b39 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -479,7 +479,7 @@ class RecoveryEmailService { $delay = 1 - ($now - $oldestRequest); if ($delay > 0) { - usleep((int)($delay * 1_000_000)); // Convert to microseconds and delay + usleep((int)($delay * 1000000)); // Convert to microseconds and delay } } -- GitLab From 535632e4f15573eb334011c6833ecf4a58d74d06 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 10:44:53 +0530 Subject: [PATCH 50/74] delay until next available slot --- lib/Service/RecoveryEmailService.php | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 18c2b39..7d0b60b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -467,24 +467,30 @@ class RecoveryEmailService { private function ensureRealTimeRateLimit(string $key, int $rateLimit): void { $now = microtime(true); $requests = $this->cache->get($key) ?? []; - - // Filter out requests older than 1 second (sliding window) + + // Filter out requests older than the sliding window of 1 second $requests = array_filter($requests, function ($timestamp) use ($now) { return ($now - $timestamp) <= 1; }); - if (count($requests) >= $rateLimit) { - // Calculate delay to maintain rate limit + // If we exceed the rate limit, delay until the next available slot + while (count($requests) >= $rateLimit) { $oldestRequest = min($requests); - $delay = 1 - ($now - $oldestRequest); + $delay = 1 - ($now - $oldestRequest); // Time to wait until the sliding window resets if ($delay > 0) { - usleep((int)($delay * 1000000)); // Convert to microseconds and delay + usleep((int)($delay * 1000000)); // Sleep for the calculated delay } + + // Update current time after delay and re-check + $now = microtime(true); + $requests = array_filter($requests, function ($timestamp) use ($now) { + return ($now - $timestamp) <= 1; + }); } // Add the current request timestamp - $requests[] = microtime(true); + $requests[] = $now; $this->cache->set($key, $requests, 2); } } -- GitLab From dbc5a766885b18244eba3a0434e259e40bb0ec5e Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 10:51:49 +0530 Subject: [PATCH 51/74] added maxtries to exit --- lib/Service/RecoveryEmailService.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 7d0b60b..6127901 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -464,8 +464,9 @@ class RecoveryEmailService { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } - private function ensureRealTimeRateLimit(string $key, int $rateLimit): void { + private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 5): void { $now = microtime(true); + $attempts = 0; // Track the number of attempts $requests = $this->cache->get($key) ?? []; // Filter out requests older than the sliding window of 1 second @@ -479,7 +480,7 @@ class RecoveryEmailService { $delay = 1 - ($now - $oldestRequest); // Time to wait until the sliding window resets if ($delay > 0) { - usleep((int)($delay * 1000000)); // Sleep for the calculated delay + usleep((int)($delay * 1_000_000)); // Sleep for the calculated delay } // Update current time after delay and re-check @@ -487,6 +488,13 @@ class RecoveryEmailService { $requests = array_filter($requests, function ($timestamp) use ($now) { return ($now - $timestamp) <= 1; }); + + // Increment attempts and check for max retries + $attempts++; + if ($attempts >= $maxRetries) { + $this->logger->info("Rate limit exceeded after $maxRetries attempts. Please try again later."); + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); + } } // Add the current request timestamp -- GitLab From 0a5372ab278d35cf1be2afb6dc207ae35d7726f0 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 11:35:30 +0530 Subject: [PATCH 52/74] updated readm --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index b6fe6bf..dedab2a 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,26 @@ 4. For html files, make sure that traslation strings are not directly assigned to an attribute. Use a tmp variable to call `t()` and then assign this variable to the attribute. 4. Commit the updated `translationfiles/templates/email-recovery.pot` 5. Translate in [Weblate](https://i18n.e.foundation/projects/ecloud/email-recovery/) + +## Recovery Email verification +1. Recovery Email Verification Diagram +flowchart TD +A[IsBlacklistedEmail] --> B[Check disposable domains gotten from GitHub] +B --> |Valid| C[Check if popular domain in `popular_domains.json` and `custom_disposable_domains.json`] +B --> |Invalid| J[Throw `BlacklistedEmailException`] +C --> |Yes| H +C --> |No| D[check if domain is valid against verifymail.io] +D --> |Invalid| E[Store domain and `related_domains` in `custom_disposable_domains.json`] +E --> J +D --> |Valid| H[check if email address is valid against verifymail.io] +H --> |Valid| I[return true] +H --> |Invalid| J + +2. Add VerifyMail API key in config using occ +`occ config:system:set verify_mail_api_key --value='[ADD API KEY]'` + +3. Run command to set Create Popular domain + +`occ email-recovery:create-popular-domains` + + -- GitLab From cd420c046a1e221f46832cea7b547c5a01a4d44c Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 11:38:09 +0530 Subject: [PATCH 53/74] apply suggestions --- lib/Service/RecoveryEmailService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 6127901..f1ec70c 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -130,7 +130,6 @@ class RecoveryEmailService { if (empty($apiKey)) { $this->logger->info('VerifyMail API Key is not configured.'); - throw new \RuntimeException($l->t('Error! User email address cannot be saved as recovery email address!')); } // Check if it's a popular domain and not custom blacklist, then verify the email @@ -480,7 +479,7 @@ class RecoveryEmailService { $delay = 1 - ($now - $oldestRequest); // Time to wait until the sliding window resets if ($delay > 0) { - usleep((int)($delay * 1_000_000)); // Sleep for the calculated delay + usleep((int)($delay * 1000000)); // Sleep for the calculated delay } // Update current time after delay and re-check -- GitLab From de4eb181337bbb5c22ffe9bbee01cb400e05e730 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 11:41:13 +0530 Subject: [PATCH 54/74] apply suggestions --- lib/Service/DomainService.php | 36 +++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index d741133..dcc2ed7 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -105,22 +105,26 @@ class DomainService { } /** - * Get domains from a file. - */ - private function getDomainsFromFile(string $filename): array { - try { - $file = $this->getDomainsFile($filename); - $content = $file->getContent(); - return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; - } catch (NotFoundException $e) { - $this->logger->warning("File $filename not found!"); - return []; - } catch (\Throwable $e) { - $this->logger->warning("Error reading $filename: " . $e->getMessage()); - return []; - } - } - + * Get domains from a file. + */ +private function getDomainsFromFile(string $filename): array { + try { + // Attempt to get and read the file + $file = $this->getDomainsFile($filename); + $content = $file->getContent(); + + // Decode JSON content + return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; + } catch (NotFoundException $e) { + // File not found, treat as no domains configured + $this->logger->warning("File $filename not found. Returning an empty domain list."); + return []; + } catch (\Throwable $e) { + // Other errors indicate a serious issue (e.g., unreadable file) + $this->logger->error("Error reading $filename: " . $e->getMessage()); + throw new \RuntimeException("Unable to read the domain file. Please check your configuration.", 0, $e); + } +} /** * Save domains to a file. */ -- GitLab From e0363a1b50122d97e1d644c84804774b787b0c89 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 11:41:24 +0530 Subject: [PATCH 55/74] apply suggestions --- lib/Service/DomainService.php | 36 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index dcc2ed7..c213fee 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -105,25 +105,25 @@ class DomainService { } /** - * Get domains from a file. - */ + * Get domains from a file. + */ private function getDomainsFromFile(string $filename): array { - try { - // Attempt to get and read the file - $file = $this->getDomainsFile($filename); - $content = $file->getContent(); - - // Decode JSON content - return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; - } catch (NotFoundException $e) { - // File not found, treat as no domains configured - $this->logger->warning("File $filename not found. Returning an empty domain list."); - return []; - } catch (\Throwable $e) { - // Other errors indicate a serious issue (e.g., unreadable file) - $this->logger->error("Error reading $filename: " . $e->getMessage()); - throw new \RuntimeException("Unable to read the domain file. Please check your configuration.", 0, $e); - } + try { + // Attempt to get and read the file + $file = $this->getDomainsFile($filename); + $content = $file->getContent(); + + // Decode JSON content + return json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; + } catch (NotFoundException $e) { + // File not found, treat as no domains configured + $this->logger->warning("File $filename not found. Returning an empty domain list."); + return []; + } catch (\Throwable $e) { + // Other errors indicate a serious issue (e.g., unreadable file) + $this->logger->error("Error reading $filename: " . $e->getMessage()); + throw new \RuntimeException("Unable to read the domain file. Please check your configuration.", 0, $e); + } } /** * Save domains to a file. -- GitLab From c07faa51e475115a6d4ef045aec54c364b154c9d Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 12:25:44 +0530 Subject: [PATCH 56/74] fix readme --- README.md | 3 ++- lib/Service/DomainService.php | 2 +- lib/Service/RecoveryEmailService.php | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dedab2a..a5f1422 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ ## Recovery Email verification 1. Recovery Email Verification Diagram +``` flowchart TD A[IsBlacklistedEmail] --> B[Check disposable domains gotten from GitHub] B --> |Valid| C[Check if popular domain in `popular_domains.json` and `custom_disposable_domains.json`] @@ -28,7 +29,7 @@ E --> J D --> |Valid| H[check if email address is valid against verifymail.io] H --> |Valid| I[return true] H --> |Invalid| J - +``` 2. Add VerifyMail API key in config using occ `occ config:system:set verify_mail_api_key --value='[ADD API KEY]'` diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index c213fee..546e070 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -122,7 +122,7 @@ private function getDomainsFromFile(string $filename): array { } catch (\Throwable $e) { // Other errors indicate a serious issue (e.g., unreadable file) $this->logger->error("Error reading $filename: " . $e->getMessage()); - throw new \RuntimeException("Unable to read the domain file. Please check your configuration.", 0, $e); + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); } } /** diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index f1ec70c..d900e83 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -135,7 +135,7 @@ class RecoveryEmailService { // Check if it's a popular domain and not custom blacklist, then verify the email if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail)) { $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); - $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey); + $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey, $l); } // Verify the domain using the API @@ -220,10 +220,10 @@ class RecoveryEmailService { throw new \RuntimeException("API call failed unexpectedly after maximum retries."); } - private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey): void { + private function ensureEmailIsValid(string $recoveryEmail, string $username, string $apiKey, IL10N $l): void { $url = sprintf(self::VERIFYMAIL_API_URL, $recoveryEmail, $apiKey); - $this->retryApiCall(function () use ($url, $username) { + $this->retryApiCall(function () use ($url, $username, $l) { try { $httpClient = $this->httpClientService->newClient(); // Make the API request @@ -257,7 +257,7 @@ class RecoveryEmailService { private function verifyDomainWithApi(string $domain, string $username, string $apiKey): void { $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); - $this->retryApiCall(function () use ($url, $username, $domain) { + $this->retryApiCall(function () use ($url, $username, $domain, $l) { $httpClient = $this->httpClientService->newClient(); // Make the API request $response = $httpClient->get($url, [ -- GitLab From 0f7df7877fd6917fd93019a1c3a7bf3f3df64a09 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 12:29:59 +0530 Subject: [PATCH 57/74] added image --- README.md | 15 ++------------- recovery-verification-steps.png | Bin 0 -> 46382 bytes 2 files changed, 2 insertions(+), 13 deletions(-) create mode 100644 recovery-verification-steps.png diff --git a/README.md b/README.md index a5f1422..1449852 100644 --- a/README.md +++ b/README.md @@ -17,19 +17,8 @@ ## Recovery Email verification 1. Recovery Email Verification Diagram -``` -flowchart TD -A[IsBlacklistedEmail] --> B[Check disposable domains gotten from GitHub] -B --> |Valid| C[Check if popular domain in `popular_domains.json` and `custom_disposable_domains.json`] -B --> |Invalid| J[Throw `BlacklistedEmailException`] -C --> |Yes| H -C --> |No| D[check if domain is valid against verifymail.io] -D --> |Invalid| E[Store domain and `related_domains` in `custom_disposable_domains.json`] -E --> J -D --> |Valid| H[check if email address is valid against verifymail.io] -H --> |Valid| I[return true] -H --> |Invalid| J -``` +![Recovery Verification Steps](recovery-verification-steps.png) + 2. Add VerifyMail API key in config using occ `occ config:system:set verify_mail_api_key --value='[ADD API KEY]'` diff --git a/recovery-verification-steps.png b/recovery-verification-steps.png new file mode 100644 index 0000000000000000000000000000000000000000..2b4542e30043383902a727544b5ebc9c64aab4bb GIT binary patch literal 46382 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 Date: Mon, 20 Jan 2025 13:04:39 +0530 Subject: [PATCH 58/74] pass --- lib/Service/DomainService.php | 17 ++++++++++------- lib/Service/RecoveryEmailService.php | 18 +++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 546e070..df547b8 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -10,12 +10,14 @@ use OCP\Files\SimpleFS\ISimpleFile; use OCP\ILogger; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use OCP\IL10N; class DomainService { private ILogger $logger; private IAppData $appData; private Client $httpClient; private string $appName; + private IL10N $l; private const POPULAR_DOMAINS_URL = 'https://gist.githubusercontent.com/mavieth/418b0ba7b3525517dd85b31ee881b2ec/raw/domains.json'; private const POPULAR_DOMAINS_FILE = 'popular_domains.json'; @@ -23,11 +25,12 @@ class DomainService { private const BLACKLISTED_DOMAINS_FILE = 'blacklisted_domains.json'; private const DISPOSABLE_DOMAINS_FILE = 'disposable_domains.json'; - public function __construct(string $appName, ILogger $logger, IAppData $appData, Client $httpClient) { + public function __construct(string $appName, ILogger $logger, IAppData $appData, Client $httpClient, IL10N $l) { $this->logger = $logger; $this->appData = $appData; $this->httpClient = $httpClient; $this->appName = $appName; + $this->l = $l; } // ------------------------------- @@ -37,7 +40,7 @@ class DomainService { /** * Check if an email belongs to a popular domain. */ - public function isPopularDomain(string $email): bool { + public function isPopularDomain(string $email, IL10N $l): bool { $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE); return $this->isDomainInList($email, $domains); } @@ -45,7 +48,7 @@ class DomainService { /** * Check if an email belongs to a blacklisted domain. */ - public function isBlacklistedDomain(string $email): bool { + public function isBlacklistedDomain(string $email, IL10N $l): bool { $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE); return $this->isDomainInList($email, $domains); } @@ -53,7 +56,7 @@ class DomainService { /** * Check if an email belongs to a custom blacklist domain. */ - public function isDomainInCustomBlacklist(string $email): bool { + public function isDomainInCustomBlacklist(string $email, IL10N $l): bool { $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); return $this->isDomainInList($email, $domains); } @@ -68,8 +71,8 @@ class DomainService { /** * Add a custom disposable domain to the list. */ - public function addCustomDisposableDomain(string $domain, array $relatedDomains = []): void { - $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); + public function addCustomDisposableDomain(string $domain, array $relatedDomains = [], l10N $l): void { + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE, $l); $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); } @@ -107,7 +110,7 @@ class DomainService { /** * Get domains from a file. */ -private function getDomainsFromFile(string $filename): array { +private function getDomainsFromFile(string $filename, IL10N $l): array { try { // Attempt to get and read the file $file = $this->getDomainsFile($filename); diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index d900e83..427cb56 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -133,7 +133,7 @@ class RecoveryEmailService { } // Check if it's a popular domain and not custom blacklist, then verify the email - if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail)) { + if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail, $l)) { $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey, $l); } @@ -141,7 +141,7 @@ class RecoveryEmailService { // Verify the domain using the API $this->ensureRealTimeRateLimit(self::RATE_LIMIT_DOMAIN, 15); $domain = substr(strrchr($recoveryEmail, "@"), 1); - $this->verifyDomainWithApi($domain, $username, $apiKey); + $this->verifyDomainWithApi($domain, $username, $apiKey, $l); return true; } @@ -178,7 +178,7 @@ class RecoveryEmailService { throw new MurenaDomainDisallowedException($l->t('You cannot set an email address with a Murena domain as recovery email address.')); } - if ($this->domainService->isBlacklistedDomain($recoveryEmail)) { + if ($this->domainService->isBlacklistedDomain($recoveryEmail, $l)) { $this->logger->info("User ID $username's requested recovery email address domain is blacklisted."); throw new BlacklistedEmailException($l->t('The domain of this email address is blacklisted. Please provide another recovery address.')); } @@ -237,7 +237,7 @@ class RecoveryEmailService { if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is disposable."); - throw new BlacklistedEmailException($l->t('"The email address is disposable. Please provide another recovery address.')); + throw new BlacklistedEmailException($l->t('The email address is disposable. Please provide another recovery address.')); } if (!$data['deliverable_email'] ?? true) { @@ -254,7 +254,7 @@ class RecoveryEmailService { - private function verifyDomainWithApi(string $domain, string $username, string $apiKey): void { + private function verifyDomainWithApi(string $domain, string $username, string $apiKey, l10N $l): void { $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); $this->retryApiCall(function () use ($url, $username, $domain, $l) { @@ -277,14 +277,14 @@ class RecoveryEmailService { // Handle response data if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); - $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); - throw new BlacklistedEmailException("The domain of this email address is disposable. Please provide another recovery address."); + $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? [], $l); + throw new BlacklistedEmailException($l->t('The email address is disposable. Please provide another recovery address.')); } if (!$data['mx'] ?? true) { $this->logger->info("User ID $username's requested recovery email address domain is not valid."); - $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? []); - throw new BlacklistedEmailException("The domain of this email address is invalid. Please provide another recovery address."); + $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? [], $l); + throw new BlacklistedEmailException($l->t('The email address is not deliverable. Please provide another recovery address.')); } $this->logger->info("User ID $username's requested recovery email address domain is valid."); -- GitLab From 0a0922ac0277ea6ab61dc92fb6f2d6d6d5229b36 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 13:06:03 +0530 Subject: [PATCH 59/74] maxtries 10 --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 427cb56..e8cc052 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -463,7 +463,7 @@ class RecoveryEmailService { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } - private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 5): void { + private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 10): void { $now = microtime(true); $attempts = 0; // Track the number of attempts $requests = $this->cache->get($key) ?? []; -- GitLab From d2dd2023f35173804466ffcc6465466ae2b487c1 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 13:06:49 +0530 Subject: [PATCH 60/74] maxtries 10 --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index e8cc052..b69c1bf 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -184,7 +184,7 @@ class RecoveryEmailService { } } - private function retryApiCall(callable $callback, int $maxRetries = 5, int $initialInterval = 1000): void { + private function retryApiCall(callable $callback, int $maxRetries = 10, int $initialInterval = 1000): void { $retryInterval = $initialInterval; // Initial retry interval in milliseconds $retries = 0; -- GitLab From fa2f91d1bd26c4ff9bda02f03219a6d615a94e98 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 13:14:28 +0530 Subject: [PATCH 61/74] fix error in parameter --- lib/Service/DomainService.php | 2 +- lib/Service/RecoveryEmailService.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index df547b8..3def887 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -71,7 +71,7 @@ class DomainService { /** * Add a custom disposable domain to the list. */ - public function addCustomDisposableDomain(string $domain, array $relatedDomains = [], l10N $l): void { + public function addCustomDisposableDomain(string $domain, l10N $l, array $relatedDomains = []): void { $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE, $l); $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index b69c1bf..958a9b2 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -277,13 +277,13 @@ class RecoveryEmailService { // Handle response data if ($data['disposable'] ?? false) { $this->logger->info("User ID $username's requested recovery email address is from a disposable domain."); - $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? [], $l); + $this->domainService->addCustomDisposableDomain($domain, $l, $data['related_domains'] ?? []); throw new BlacklistedEmailException($l->t('The email address is disposable. Please provide another recovery address.')); } if (!$data['mx'] ?? true) { $this->logger->info("User ID $username's requested recovery email address domain is not valid."); - $this->domainService->addCustomDisposableDomain($domain, $data['related_domains'] ?? [], $l); + $this->domainService->addCustomDisposableDomain($domain, $l, $data['related_domains'] ?? []); throw new BlacklistedEmailException($l->t('The email address is not deliverable. Please provide another recovery address.')); } -- GitLab From 659b5c9e0b56999e13b30bab788c04830925ee5f Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 13:26:05 +0530 Subject: [PATCH 62/74] pass parameters --- lib/Service/DomainService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 3def887..8183953 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -41,7 +41,7 @@ class DomainService { * Check if an email belongs to a popular domain. */ public function isPopularDomain(string $email, IL10N $l): bool { - $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE); + $domains = $this->getDomainsFromFile(self::POPULAR_DOMAINS_FILE, $l); return $this->isDomainInList($email, $domains); } @@ -49,7 +49,7 @@ class DomainService { * Check if an email belongs to a blacklisted domain. */ public function isBlacklistedDomain(string $email, IL10N $l): bool { - $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE); + $domains = $this->getDomainsFromFile(self::BLACKLISTED_DOMAINS_FILE, $l); return $this->isDomainInList($email, $domains); } @@ -57,7 +57,7 @@ class DomainService { * Check if an email belongs to a custom blacklist domain. */ public function isDomainInCustomBlacklist(string $email, IL10N $l): bool { - $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE); + $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE, $l); return $this->isDomainInList($email, $domains); } -- GitLab From bbca135a10a80482a0a5105d99acbd0aeb78d619 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 13:43:53 +0530 Subject: [PATCH 63/74] fix error --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 958a9b2..29edd64 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -133,7 +133,7 @@ class RecoveryEmailService { } // Check if it's a popular domain and not custom blacklist, then verify the email - if ($this->domainService->isPopularDomain($recoveryEmail) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail, $l)) { + if ($this->domainService->isPopularDomain($recoveryEmail, $l) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail, $l)) { $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey, $l); } -- GitLab From 7f2040ea87ff1a1558749255679cdd8cd3cd6525 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 13:56:08 +0530 Subject: [PATCH 64/74] pass parameters --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 29edd64..748775b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -254,7 +254,7 @@ class RecoveryEmailService { - private function verifyDomainWithApi(string $domain, string $username, string $apiKey, l10N $l): void { + private function verifyDomainWithApi(string $domain, string $username, string $apiKey, IL10N $l): void { $url = sprintf(self::VERIFYMAIL_API_URL, $domain, $apiKey); $this->retryApiCall(function () use ($url, $username, $domain, $l) { -- GitLab From 4dc52dca8a084ac3f9197867f8185a4d426db133 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 14:01:53 +0530 Subject: [PATCH 65/74] fix IL10N --- lib/Service/DomainService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 8183953..ca5f43c 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -71,7 +71,7 @@ class DomainService { /** * Add a custom disposable domain to the list. */ - public function addCustomDisposableDomain(string $domain, l10N $l, array $relatedDomains = []): void { + public function addCustomDisposableDomain(string $domain, IL10N $l, array $relatedDomains = []): void { $domains = $this->getDomainsFromFile(self::DISPOSABLE_DOMAINS_FILE, $l); $newDomains = array_unique(array_merge($domains, [$domain], $relatedDomains)); $this->saveDomainsToFile(self::DISPOSABLE_DOMAINS_FILE, $newDomains); -- GitLab From 5a70c1b21bba5e3de58d9636359700f1701c640e Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 14:35:20 +0530 Subject: [PATCH 66/74] fix french trans --- l10n/fr.js | 2 +- l10n/fr.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/l10n/fr.js b/l10n/fr.js index 90ad988..8a5e027 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -37,6 +37,6 @@ OC.L10N.register( "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale.", "The email could not be verified. Please try again later.": "L'e-mail n'a pas pu être vérifié. Veuillez réessayer plus tard.", "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", - "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." + "The email address is not deliverable. Please provide another recovery address.": "L'adresse électronique ne peut être délivrée. Veuillez fournir une autre adresse de recouvrement." }, "nplurals=2; plural=n > 1;"); diff --git a/l10n/fr.json b/l10n/fr.json index 3602a0d..ed35292 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -35,6 +35,6 @@ "This version of Murena Workspace allows a minimal access only. This means some configuration you set previously (e.g. additional mail accounts) as well as some functionalities (e.g. Files, PGP) may not be there. You'll find them back when everything goes back to normal.": "Cette version de Murena Workspace permet uniquement un accès minimal. Cela signifie que certaines configurations que vous avez définies précédemment (comme des comptes de messagerie supplémentaires) ainsi que certaines fonctionnalités (comme Fichiers, PGP) peuvent ne pas être présentes. Vous les retrouverez lorsque tout reviendra à la normale.", "The email could not be verified. Please try again later.": "L'e-mail n'a pas pu être vérifié. Veuillez réessayer plus tard.", "The email address is disposable. Please provide another recovery address." : "L'adresse électronique est jetable. Veuillez fournir une autre adresse de récupération.", - "The email address is not deliverable. Please provide another recovery address.": "The email address is not deliverable. Please provide another recovery address." + "The email address is not deliverable. Please provide another recovery address.": "L'adresse électronique ne peut être délivrée. Veuillez fournir une autre adresse de recouvrement." },"pluralForm" :"nplurals=2; plural=n > 1;" } \ No newline at end of file -- GitLab From 9fc1458d31149dc127b5401dfe098adad5b363cb Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 15:03:56 +0530 Subject: [PATCH 67/74] applied suggestion --- lib/Command/CreatePopularDomain.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Command/CreatePopularDomain.php b/lib/Command/CreatePopularDomain.php index 43bb5b2..a19f96b 100644 --- a/lib/Command/CreatePopularDomain.php +++ b/lib/Command/CreatePopularDomain.php @@ -16,7 +16,6 @@ class CreatePopularDomain extends Command { private DomainService $domainService; private ILogger $logger; private IAppData $appData; - private const POPULAR_DOMAINS_FILE_NAME = 'domains.json'; public function __construct(DomainService $domainService, ILogger $logger, IAppData $appData) { parent::__construct(); -- GitLab From 558bb9244016abbebe132214da9b1e5219ae63cd Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 15:14:27 +0530 Subject: [PATCH 68/74] pass --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 748775b..2d5431b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -184,7 +184,7 @@ class RecoveryEmailService { } } - private function retryApiCall(callable $callback, int $maxRetries = 10, int $initialInterval = 1000): void { + private function retryApiCall(callable $callback, int $maxRetries = 10, int $initialInterval = 1000, IL10N $l): void { $retryInterval = $initialInterval; // Initial retry interval in milliseconds $retries = 0; -- GitLab From c58bf4d9644e0fbc5335758dd6ced146ed5b0435 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 15:19:04 +0530 Subject: [PATCH 69/74] declare cache --- lib/Service/RecoveryEmailService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 2d5431b..7cbc26a 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -47,6 +47,7 @@ class RecoveryEmailService { private const VERIFYMAIL_API_URL = 'https://verifymail.io/api/%s?key=%s'; private const RATE_LIMIT_EMAIL = 'verifymail_email_ratelimit'; private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; + private $cache; private DomainService $domainService; private IL10N $l; -- GitLab From b8e5855966284700cf7b61ab5910bb441bf95dd1 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 16:32:13 +0530 Subject: [PATCH 70/74] fixed --- lib/Service/RecoveryEmailService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 7cbc26a..3d1aa18 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -135,12 +135,12 @@ class RecoveryEmailService { // Check if it's a popular domain and not custom blacklist, then verify the email if ($this->domainService->isPopularDomain($recoveryEmail, $l) && !$this->domainService->isDomainInCustomBlacklist($recoveryEmail, $l)) { - $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2); + $this->ensureRealTimeRateLimit(self::RATE_LIMIT_EMAIL, 2, $l); $this->ensureEmailIsValid($recoveryEmail, $username, $apiKey, $l); } // Verify the domain using the API - $this->ensureRealTimeRateLimit(self::RATE_LIMIT_DOMAIN, 15); + $this->ensureRealTimeRateLimit(self::RATE_LIMIT_DOMAIN, 15, $l); $domain = substr(strrchr($recoveryEmail, "@"), 1); $this->verifyDomainWithApi($domain, $username, $apiKey, $l); @@ -464,7 +464,7 @@ class RecoveryEmailService { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } - private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 10): void { + private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 10, IL10N $l): void { $now = microtime(true); $attempts = 0; // Track the number of attempts $requests = $this->cache->get($key) ?? []; -- GitLab From ffff553cfe205fa4d593bb3f314f11c620f51d88 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 16:36:03 +0530 Subject: [PATCH 71/74] fixed --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 3d1aa18..9c3aabc 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -464,7 +464,7 @@ class RecoveryEmailService { return filter_var($recoveryEmail, FILTER_VALIDATE_EMAIL) !== false; } - private function ensureRealTimeRateLimit(string $key, int $rateLimit, int $maxRetries = 10, IL10N $l): void { + private function ensureRealTimeRateLimit(string $key, int $rateLimit, IL10N $l, int $maxRetries = 10): void { $now = microtime(true); $attempts = 0; // Track the number of attempts $requests = $this->cache->get($key) ?? []; -- GitLab From 426c3d60d33d557ca678ddafee869b3120814dd8 Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 16:50:33 +0530 Subject: [PATCH 72/74] required parameter fix --- lib/Service/RecoveryEmailService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 9c3aabc..31256af 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -185,7 +185,7 @@ class RecoveryEmailService { } } - private function retryApiCall(callable $callback, int $maxRetries = 10, int $initialInterval = 1000, IL10N $l): void { + private function retryApiCall(callable $callback, IL10N $l, int $maxRetries = 10, int $initialInterval = 1000): void { $retryInterval = $initialInterval; // Initial retry interval in milliseconds $retries = 0; -- GitLab From 51f886a1d614f4a62c515ad8de91d9697e9a595f Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 17:06:00 +0530 Subject: [PATCH 73/74] required parameter fix --- lib/Service/RecoveryEmailService.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 31256af..c13299b 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -250,7 +250,9 @@ class RecoveryEmailService { $this->logger->error("Error while validating email for user $username: " . $e->getMessage()); throw $e; // Re-throw if necessary } - }); + }, $l, // Pass the IL10N object + 5, // Optional: Max retries (default is 10, override if necessary) + 1000); } @@ -289,7 +291,9 @@ class RecoveryEmailService { } $this->logger->info("User ID $username's requested recovery email address domain is valid."); - }); + }, $l, // Pass the IL10N object + 5, // Optional: Max retries (default is 10, override if necessary) + 1000); } -- GitLab From 69606eb3178f1d8065d584201973a04da93d605f Mon Sep 17 00:00:00 2001 From: Avinash Gusain Date: Mon, 20 Jan 2025 17:07:03 +0530 Subject: [PATCH 74/74] required parameter fix --- lib/Service/RecoveryEmailService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index c13299b..74a4f61 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -251,7 +251,7 @@ class RecoveryEmailService { throw $e; // Re-throw if necessary } }, $l, // Pass the IL10N object - 5, // Optional: Max retries (default is 10, override if necessary) + 10, // Optional: Max retries (default is 10, override if necessary) 1000); } @@ -292,7 +292,7 @@ class RecoveryEmailService { $this->logger->info("User ID $username's requested recovery email address domain is valid."); }, $l, // Pass the IL10N object - 5, // Optional: Max retries (default is 10, override if necessary) + 10, // Optional: Max retries (default is 10, override if necessary) 1000); } -- GitLab