diff --git a/appinfo/info.xml b/appinfo/info.xml index e24e90e1610da3811244550aa12ca9b2515be969..bb516447788802e3b6b5ed39307916b348296088 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Email Recovery Email Recovery App - 11.0.1 + 11.0.2 agpl MURENA SAS EmailRecovery diff --git a/lib/Db/ConfigMapper.php b/lib/Db/ConfigMapper.php index b7f3484064392ccc0b3a90126127dd2757a7cffb..3248d7c062718601bb12abfdf17feaa3a1e041e6 100644 --- a/lib/Db/ConfigMapper.php +++ b/lib/Db/ConfigMapper.php @@ -14,20 +14,33 @@ class ConfigMapper { $this->appName = $appName; } - - public function getUsersByRecoveryEmail(string $pattern) : array { + private function countRecoveryEmailEntry(string $pattern, string $operation): int { $pattern = strtolower($pattern); $qb = $this->connection->getQueryBuilder(); - $qb->select('userid') - ->from("preferences")->where('LOWER(`configvalue`) like :pattern AND (`configkey` = "unverified-recovery-email" OR `configkey` = "recovery-email") AND `appid` = :appname ') + $qb->select($qb->func()->count('userid')) + ->from("preferences")->where("LOWER(`configvalue`) $operation :pattern AND (`configkey` = 'unverified-recovery-email' OR `configkey` = 'recovery-email') AND `appid` = :appname ") ->setParameter('pattern', $pattern) ->setParameter('appname', $this->appName); - $result = $qb->execute(); - $userIDs = []; - while ($row = $result->fetch()) { - $userIDs[] = $row['userid']; + + $result = $qb->executeQuery(); + $count = $result->fetchOne(); + $result->closeCursor(); + + if ($count !== false) { + $count = (int)$count; + } else { + $count = 0; } - return $userIDs; + + return $count; + } + + public function countRecoveryEmail(string $pattern): int { + return $this->countRecoveryEmailEntry($pattern, "like"); + } + + public function countRecoveryEmailByRegex(string $pattern): int { + return $this->countRecoveryEmailEntry($pattern, "REGEXP"); } public function getAllVerifiedRecoveryEmails(): array { diff --git a/lib/Service/RecoveryEmailService.php b/lib/Service/RecoveryEmailService.php index 69a7e376a7cc44488a8a3aa823d6b74c8f4223f3..e213545ac906d9d100b231429a4b83ccf5dcfd41 100644 --- a/lib/Service/RecoveryEmailService.php +++ b/lib/Service/RecoveryEmailService.php @@ -52,6 +52,12 @@ class RecoveryEmailService { private const RATE_LIMIT_DOMAIN = 'verifymail_domain_ratelimit'; private const RECOVERY_EMAIL_REMINDER_START_DATE = 'recovery-email-reminder-start-date'; private const UNVERIFIED_USER_DISABLE_AT = 'unverified-user-disabled-at'; + private const GMAIL_QUERY_LAST_PART = "(@gmail.com|@googlemail.com)"; //regex: is the str ends with any gmail's domains + private const GMAIL_QUERY_MIDDLE_PART = "\+[^@]+"; //regex: for alias email addresses, ignore anything after + + private const GMAIL_DOMAINS = [ + 'gmail.com', + 'googlemail.com', + ]; private $cache; @@ -190,6 +196,11 @@ class RecoveryEmailService { throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.')); } + if ($this->isGmailRecoveryEmailTaken($username, $recoveryEmail)) { + $this->logger->info("User ID $username's requested recovery email (gmail) address is already taken"); + throw new RecoveryEmailAlreadyFoundException($l->t('Recovery email address is already taken.')); + } + if (!$this->isAliasedRecoveryEmailValid($username, $recoveryEmail)) { $this->logger->info("User ID $username's requested recovery extended email address is already taken"); throw new RecoveryEmailAlreadyFoundException($l->t('This email address in invalid, please use another one.')); @@ -355,11 +366,12 @@ class RecoveryEmailService { return false; } - private function getUsernameAndDomain(string $email) : ?array { + private function getUsernameAndDomain(?string $email) : ?array { if ($email === null || empty($email)) { return null; } $email = strtolower($email); + $email = trim($email); $emailParts = explode('@', $email); $mailUsernameParts = explode('+', $emailParts[0]); $mailUsername = $mailUsernameParts[0]; @@ -367,6 +379,87 @@ class RecoveryEmailService { return [$mailUsername, $mailDomain]; } + /* + * if the provided recoveryMail is gmail check if this is already taken or not. + * return false if the recoveryMail is not gmail / alias mail / not taken + */ + private function isGmailRecoveryEmailTaken(string $username, string $recoveryEmail): bool { + // if alias found in the mail address, ignore for now, it will be handle in the next section of the caller + if (str_contains($recoveryEmail, '+')) { + return false; + } + + $mailPrefix = $this->getGmailUsername($recoveryEmail); + // provided recoveryEmail is not gmail. + if ($mailPrefix === null) { + return false; + } + + if ($this->isRecoveryGmailBelongsToUser($username, $mailPrefix)) { + return false; + } + + $regex = $this->getGmailQueryRegexFirstPart($mailPrefix) . self::GMAIL_QUERY_LAST_PART; + + $recoveryEmailCount = $this->configMapper->countRecoveryEmailByRegex($regex); + return $recoveryEmailCount > 0; + } + + /** + * if provided user already has same gmail account setup as recoveryEmail / unverifiedRecoveryEmail, + * then we assume that is already passed verification once. + * So no need verfication run again. + */ + private function isRecoveryGmailBelongsToUser(string $username, string $mailPrefix): bool { + $currentRecoveryEmail = $this->getRecoveryEmail($username); + $currentUnverifiedRecoveryEmail = $this->getUnverifiedRecoveryEmail($username); + $currentRecoveryEmailUserPart = $this->getGmailUsername($currentRecoveryEmail); + $currentUnverifiedRecoveryEmailUserPart = $this->getGmailUsername($currentUnverifiedRecoveryEmail); + + // requested recoveryEmail is already set by the current user + return (($mailPrefix === $currentRecoveryEmailUserPart) || ($mailPrefix === $currentUnverifiedRecoveryEmailUserPart)); + } + + /** + * return sanitized mailPrefix if provided mail address is gmail + * gmail doesn't care about (.) in the mail address. + */ + private function getGmailUsername(?string $email): ?string { + $emailParts = $this->getUsernameAndDomain($email); + + if ($emailParts == null || !in_array($emailParts[1], self::GMAIL_DOMAINS)) { + return null; + } + + return str_replace(".", "", $emailParts[0]); + } + + /** + * gmail doesn't care about (.) in the mail address. + * So, to retrieve all possible entries using regex from Database, we need to modify the mail prefix + * ex: if the mail address is abc@gmail.com, then the mailPrefix is abc. So, this method will return ^a[.]?b[.]?c[.]? + */ + private function getGmailQueryRegexFirstPart(string $mailPrefix): string { + $regex = "^"; + + foreach (str_split($mailPrefix) as $c) { + $regex = $regex . $c . "[.]?"; + } + + return $regex; + } + + private function isGmailAliasedRecoveryEmailValid(string $username, string $mailPrefix, int $emailAliasLimit): bool { + if ($this->isRecoveryGmailBelongsToUser($username, $mailPrefix)) { + return true; + } + + $regex = $this->getGmailQueryRegexFirstPart($mailPrefix) . self::GMAIL_QUERY_MIDDLE_PART . self::GMAIL_QUERY_LAST_PART; + + $recoveryEmailCount = $this->configMapper->countRecoveryEmailByRegex($regex); + return $recoveryEmailCount < $emailAliasLimit; + } + public function isAliasedRecoveryEmailValid(string $username, string $recoveryEmail): bool { if (!str_contains($recoveryEmail, '+')) { return true; @@ -376,6 +469,12 @@ class RecoveryEmailService { if ($emailAliasLimit === -1) { return true; } + + $gmailPrefix = $this->getGmailUsername($recoveryEmail); + if ($gmailPrefix !== null) { + return $this->isGmailAliasedRecoveryEmailValid($username, $gmailPrefix, $emailAliasLimit); + } + $recoveryEmailregex = $recoveryEmailParts[0]."+%@".$recoveryEmailParts[1]; $currentRecoveryEmail = $this->getRecoveryEmail($username); $currentUnverifiedRecoveryEmail = $this->getUnverifiedRecoveryEmail($username); @@ -387,12 +486,8 @@ class RecoveryEmailService { return true; } - $usersWithEmailRecovery = $this->configMapper->getUsersByRecoveryEmail($recoveryEmailregex); - if (count($usersWithEmailRecovery) > $emailAliasLimit) { - return false; - } - - return true; + $recoveryEmailCount = $this->configMapper->countRecoveryEmail($recoveryEmailregex); + return $recoveryEmailCount < $emailAliasLimit; } public function updateRecoveryEmail(string $username, string $recoveryEmail) : void {