diff --git a/README.md b/README.md index cb8ba04021ea3e1a27ee87cabc42e298692bf89b..da31fe2f89c64c1664f5603f4921060a22ea3173 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,14 @@ To delete a config value: occ config:system:delete recovery_warning_configs ``` +## Blacklisting domains for recovery email + +- There is inbuilt automated blacklisting of domains in this apps based on multiple sources +- The command `occ email-recovery:admin-blacklisted-domains` can be used by an admin to manage manual blacklisting of domains + - `occ email-recovery:admin-blacklisted-domains --list` lists all the domains blacklisted by admin + - `occ email-recovery:admin-blacklisted-domains --add domain.xyz` can be used to add the domain "domain.xyz" to the admin blacklist + - `occ email-recovery:admin-blacklisted-domains --delete domain.xyz` can be used to delete the domain "domain.xyz" from the admin blacklist + ## OCC Commands The recovery warning notification system has been implemented as an OCC command for better control and reliability. @@ -143,4 +151,4 @@ occ email-recovery:recovery-warning-notification 2>> /var/log/nextcloud/recovery # Log with timestamps occ email-recovery:recovery-warning-notification 2>&1 | while IFS= read -r line; do echo "$(date '+%Y-%m-%d %H:%M:%S') $line"; done >> /var/log/nextcloud/recovery-timestamped.log -``` \ No newline at end of file +``` diff --git a/appinfo/info.xml b/appinfo/info.xml index f0d156f5fd65ab2f71fd7b14fc9e4721d7dc168d..cda7e90aada4536354f19d19f4ef2b05a282eb23 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,6 +22,7 @@ OCA\EmailRecovery\Command\CreatePopularDomain OCA\EmailRecovery\Command\SpamAccountDetection OCA\EmailRecovery\Command\ResetDisposableDomainsList + OCA\EmailRecovery\Command\AdminBlacklistedDomains OCA\EmailRecovery\Command\RecoveryWarningNotificationCommand diff --git a/lib/Command/AdminBlacklistedDomains.php b/lib/Command/AdminBlacklistedDomains.php new file mode 100644 index 0000000000000000000000000000000000000000..b17eeea1ea5b6816da40d4235c4d381b7b95e440 --- /dev/null +++ b/lib/Command/AdminBlacklistedDomains.php @@ -0,0 +1,101 @@ +setName(Application::APP_ID.':admin-blacklisted-domains') + ->setDescription('Manage admin blacklisted 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 blacklisted 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 blacklisted domains: ' . $e->getMessage()); + return 1; + } + try { + if ($input->getOption('add')) { + $this->addDomain($input, $output); + return 0; + } + } catch (\Throwable $e) { + $output->writeln("Error adding admin blacklisted domain: " . $e->getMessage()); + return 1; + } + + try { + if ($input->getOption('delete')) { + $this->deleteDomain($input, $output); + return 0; + } + } catch (\Throwable $e) { + $output->writeln("Error deleting admin blacklisted domain:" . $e->getMessage()); + return 1; + } + } + + private function listDomains(OutputInterface $output): void { + $domains = $this->domainService->getAdminBlacklistedDomains(); + $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->addAdminBlacklistedDomain($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->deleteAdminBlacklistedDomain($domain); + } +} diff --git a/lib/Service/DomainService.php b/lib/Service/DomainService.php index ad0d55cc7b257c059d21216992e39ec0448bd86e..53ec8bb7652a67f6f69bbec440a90e1875a389dc 100644 --- a/lib/Service/DomainService.php +++ b/lib/Service/DomainService.php @@ -24,6 +24,7 @@ class DomainService { 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'; + private const ADMIN_BLACKLISTED_DOMAINS_FILE = 'admin_blacklisted_domains.json'; public function __construct(string $appName, LoggerInterface $logger, IAppData $appData, Client $httpClient, IL10N $l) { $this->logger = $logger; @@ -61,6 +62,49 @@ class DomainService { return $this->isDomainInList($email, $domains); } + /** + * Check if an email domain is blacklisted by admin action + */ + public function isDomainInAdminBlacklist(string $email, IL10N $l): bool { + $domains = $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE, $l); + return $this->isDomainInList($email, $domains); + } + + /** + * Add domain to admin blacklist + */ + public function addAdminBlacklistedDomain(string $domain): void { + $domains = $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE); + + $cleanDomain = preg_replace('/[\x00-\x1F\x7F]/', '', $domain); + $domains = array_unique(array_merge($domains, [$cleanDomain])); + + $this->saveDomainsToFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE, $domains); + } + + /** + * Delete domain from admin blacklist + */ + public function deleteAdminBlacklistedDomain(string $domain): void { + $domains = $this->getDomainsFromFile(self::ADMIN_BLACKLISTED_DOMAINS_FILE); + $cleanDomain = preg_replace('/[\x00-\x1F\x7F]/', '', $domain); + + $domains = array_unique($domains); + $key = array_search($domain, $domains); + + if ($key !== false) { + unset($domains[$key]); + } + + $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); + } /** * Update blacklisted domains by fetching from the external source. */ @@ -125,7 +169,7 @@ class DomainService { /** * Get domains from a file. */ - private function getDomainsFromFile(string $filename, IL10N $l): array { + private function getDomainsFromFile(string $filename, ?IL10N $l = null): array { try { // Attempt to get and read the file $file = $this->getDomainsFile($filename); @@ -142,7 +186,10 @@ class DomainService { } catch (\Throwable $e) { // Other errors indicate a serious issue (e.g., unreadable file) $this->logger->error("Error reading $filename: " . $e->getMessage()); - throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); + if ($l) { + throw new \RuntimeException($l->t('The email could not be verified. Please try again later.')); + } + throw new \RuntimeException("Error fetching domains from file $filename: " . $e->getMessage()); } } /** diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 9bbfcf141c97e55e16115b1c0177e9f6dd8efd51..ca8e619f4958f1bb82aaaeaa28a42cedc06bc00e 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -211,7 +211,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, $l)) { + if ($this->domainService->isBlacklistedDomain($recoveryEmail, $l) || $this->domainService->isDomainInAdminBlacklist($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.')); }