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 {