diff --git a/README.md b/README.md index a9c10812a92fe4e6fb658d987fc88da918c5e724..800576e8927c358a412699040c037a492d801bbc 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,14 @@ To delete a config value: occ config:system:delete recovery_warning_configs ``` +## Whitelisting domains for recovery email + +This is used to enforce a valid domain if it is blacklisted by mistake by any of our automated sources +- The command `occ email-recovery:admin-whitelisted-domains` can be used by an admin to manage manual whitelisting of domains + - `occ email-recovery:admin-whitelisted-domains --list` lists all the domains whitelisted by admin + - `occ email-recovery:admin-whitelisted-domains --add domain.xyz` can be used to add the domain "domain.xyz" to the admin whitelist + - `occ email-recovery:admin-whitelisted-domains --delete domain.xyz` can be used to delete the domain "domain.xyz" from the admin whitelist + ## Blacklisting domains for recovery email - There is inbuilt automated blacklisting of domains in this apps based on multiple sources diff --git a/appinfo/info.xml b/appinfo/info.xml index db8bbc090988df7776a87395843da53466893349..6a62bcbd520dc75fc5c30e016d770f21bddfbb54 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -25,5 +25,6 @@ OCA\EmailRecovery\Command\AdminBlacklistedDomains OCA\EmailRecovery\Command\RecoveryWarningNotificationCommand OCA\EmailRecovery\Command\FilterLegitimateDomainsFromSpamReport + OCA\EmailRecovery\Command\AdminWhitelistedDomains diff --git a/lib/Command/AdminWhitelistedDomains.php b/lib/Command/AdminWhitelistedDomains.php new file mode 100644 index 0000000000000000000000000000000000000000..c5496f62a156a32abfbff1a9a789af4a34061433 --- /dev/null +++ b/lib/Command/AdminWhitelistedDomains.php @@ -0,0 +1,102 @@ +setName(Application::APP_ID.':admin-whitelisted-domains') + ->setDescription('Manage admin whitelisted domains') + ->addArgument( + 'domain', + InputArgument::OPTIONAL, + 'Domain name to add or delete', + '' + ) + ->addOption( + 'add', + null, + InputOption::VALUE_NONE, + 'Specify this option to add the domain' + ) + ->addOption( + 'list', + null, + InputOption::VALUE_NONE, + 'Specify this option to list all whitelisted domain' + ) + ->addOption( + 'delete', + null, + InputOption::VALUE_NONE, + 'Specify this option to delete the domain' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + if ($input->getOption('list')) { + $this->listDomains($output); + return 0; + } + } catch (\Throwable $e) { + $output->writeln('Error listing admin whitelisted domains: ' . $e->getMessage()); + return 1; + } + try { + if ($input->getOption('add')) { + $this->addDomain($input, $output); + return 0; + } + } catch (\Throwable $e) { + $output->writeln("Error adding admin whitelisted domain: " . $e->getMessage()); + return 1; + } + + try { + if ($input->getOption('delete')) { + $this->deleteDomain($input, $output); + return 0; + } + } catch (\Throwable $e) { + $output->writeln("Error deleting admin whitelisted domain:" . $e->getMessage()); + return 1; + } + return 1; + } + + private function listDomains(OutputInterface $output): void { + $domains = $this->domainService->getAdminWhitelistedDomains(); + $output->write($domains, true); + } + + private function addDomain(InputInterface $input, OutputInterface $output): void { + if (!$input->getArgument('domain')) { + throw new \Exception('Domain argument is required!'); + } + $domain = $input->getArgument('domain'); + $this->domainService->addAdminWhitelistedDomain($domain); + } + + private function deleteDomain(InputInterface $input, OutputInterface $output): void { + if (!$input->getArgument('domain')) { + throw new \Exception('Domain argument is required!'); + } + $domain = $input->getArgument('domain'); + $this->domainService->deleteAdminWhitelistedDomain($domain); + } +} diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index 53ec8bb7652a67f6f69bbec440a90e1875a389dc..910699517675ca1c52098c6e50f6d02386830df6 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -25,6 +25,7 @@ class DomainService { private const BLACKLISTED_DOMAINS_FILE = 'blacklisted_domains.json'; private const DISPOSABLE_DOMAINS_FILE = 'disposable_domains.json'; private const ADMIN_BLACKLISTED_DOMAINS_FILE = 'admin_blacklisted_domains.json'; + private const ADMIN_WHITELISTED_DOMAINS_FILE = 'admin_whitelisted_domains.json'; public function __construct(string $appName, LoggerInterface $logger, IAppData $appData, Client $httpClient, IL10N $l) { $this->logger = $logger; @@ -54,6 +55,35 @@ class DomainService { return $this->isDomainInList($email, $domains); } + /** + * Check if an email belongs to a whitelisted domain. + */ + public function isAdminWhitelistedDomain(string $email, IL10N $l): bool { + $domains = $this->getDomainsFromFile(self::ADMIN_WHITELISTED_DOMAINS_FILE, $l); + return $this->isDomainInList($email, $domains); + } + + /** + * Add domain to admin blacklist + */ + public function addAdminWhitelistedDomain(string $domain): void { + $this->addDomainToFile($domain, self::ADMIN_WHITELISTED_DOMAINS_FILE); + } + + /** + * Delete domain from admin blacklist + */ + public function deleteAdminWhitelistedDomain(string $domain): void { + $this->deleteDomainFromFile($domain, self::ADMIN_WHITELISTED_DOMAINS_FILE); + } + + /** + * Return all admin blacklist domains + */ + public function getAdminWhitelistedDomains(): array { + return $this->getDomainsFromFile(self::ADMIN_WHITELISTED_DOMAINS_FILE); + } + /** * Check if an email belongs to a custom blacklist domain. */ @@ -74,19 +104,28 @@ class DomainService { * Add domain to admin blacklist */ public function addAdminBlacklistedDomain(string $domain): void { - $domains = $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE); + $this->addDomainToFile($domain, self::ADMIN_BLACKLISTED_DOMAINS_FILE); + } - $cleanDomain = preg_replace('/[\x00-\x1F\x7F]/', '', $domain); - $domains = array_unique(array_merge($domains, [$cleanDomain])); + /** + * Delete domain from admin blacklist + */ + public function deleteAdminBlacklistedDomain(string $domain): void { + $this->deleteDomainFromFile($domain, self::ADMIN_BLACKLISTED_DOMAINS_FILE); + } - $this->saveDomainsToFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE, $domains); + /** + * Return all admin blacklist domains + */ + public function getAdminBlacklistedDomains(): array { + return $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE); } /** * Delete domain from admin blacklist */ - public function deleteAdminBlacklistedDomain(string $domain): void { - $domains = $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE); + public function deleteDomainFromFile(string $domain, string $file): void { + $domains = $this->getDomainsFromFile($file); $cleanDomain = preg_replace('/[\x00-\x1F\x7F]/', '', $domain); $domains = array_unique($domains); @@ -96,15 +135,21 @@ class DomainService { unset($domains[$key]); } - $this->saveDomainsToFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE, $domains); + $this->saveDomainsToFile($file, $domains); } /** - * Return all admin blacklist domains + * Add domain to file */ - public function getAdminBlacklistedDomains(): array { - return $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE); + public function addDomainToFile(string $domain, string $file): void { + $domains = $this->getDomainsFromFile($file); + + $cleanDomain = preg_replace('/[\x00-\x1F\x7F]/', '', $domain); + $domains = array_unique(array_merge($domains, [$cleanDomain])); + + $this->saveDomainsToFile($file, $domains); } + /** * Update blacklisted domains by fetching from the external source. */ diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index b3072f0ad0762f029eeee22558b1ef134061cacf..466b34e174cb5977f12520b1f790d4b8178575b3 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -149,12 +149,17 @@ class RecoveryEmailService { if (empty($apiKey)) { $this->logger->info('VerifyMail API Key is not configured.'); } - + + // Check if domain should bypass all verification logic + if ($this->domainService->isAdminWhitelistedDomain($recoveryEmail, $l)) { + return true; + } + if ($this->domainService->isDomainInCustomBlacklist($recoveryEmail, $l)) { //throw new \Exception($l->t('The provided email domain is a disposable domain and cannot be used.')); throw new BlacklistedEmailException($l->t('The email address is disposable. Please provide another recovery address.')); } - + // Check if the domain is a popular domain if ($this->domainService->isPopularDomain($recoveryEmail, $l)) { // Skip domain verification and directly validate the email