lila/modules/security/src/main/EmailAddressValidator.scala

98 lines
3.4 KiB
Scala

package lila.security
import play.api.data.validation._
import scala.concurrent.duration._
import lila.common.EmailAddress
import lila.user.{ User, UserRepo }
/** Validate and normalize emails
*/
final class EmailAddressValidator(
userRepo: UserRepo,
disposable: DisposableEmailDomain,
dnsApi: DnsApi,
checkMail: CheckMail
) {
private def isAcceptable(email: EmailAddress): Boolean =
email.domain exists disposable.isOk
def validate(email: EmailAddress): Option[EmailAddressValidator.Acceptable] =
isAcceptable(email) option EmailAddressValidator.Acceptable(email)
/** Returns true if an E-mail address is taken by another user.
* @param email The E-mail address to be checked
* @param forUser Optionally, the user the E-mail address field is to be assigned to.
* If they already have it assigned, returns false.
* @return
*/
private def isTakenBySomeoneElse(email: EmailAddress, forUser: Option[User]): Fu[Boolean] =
userRepo.idByEmail(email.normalize) dmap (_ -> forUser) dmap {
case (None, _) => false
case (Some(userId), Some(user)) => userId != user.id
case (_, _) => true
}
private def wasUsedTwiceRecently(email: EmailAddress): Fu[Boolean] =
userRepo.countRecentByPrevEmail(email.normalize).dmap(1 <)
val acceptableConstraint = Constraint[String]("constraint.email_acceptable") { e =>
if (EmailAddress.from(e).exists(isAcceptable)) Valid
else Invalid(ValidationError("error.email_acceptable"))
}
val sendableConstraint = Constraint[String]("constraint.email_acceptable") { e =>
if (EmailAddress.from(e).exists(_.isSendable)) Valid
else Invalid(ValidationError("error.email_acceptable"))
}
def uniqueConstraint(forUser: Option[User]) =
Constraint[String]("constraint.email_unique") { e =>
val email = EmailAddress(e)
val (taken, reused) =
(isTakenBySomeoneElse(email, forUser) zip wasUsedTwiceRecently(email)).await(2 seconds, "emailUnique")
if (taken || reused) Invalid(ValidationError("error.email_unique"))
else Valid
}
def differentConstraint(than: Option[EmailAddress]) =
Constraint[String]("constraint.email_different") { e =>
if (than has EmailAddress(e))
Invalid(ValidationError("error.email_different"))
else Valid
}
// make sure the cache is warmed up, so next call can be synchronous
def preloadDns(e: EmailAddress): Funit = hasAcceptableDns(e).void
// only compute valid and non-whitelisted email domains
private def hasAcceptableDns(e: EmailAddress): Fu[Boolean] =
isAcceptable(e) ?? e.domain.map(_.lower) ?? { domain =>
if (DisposableEmailDomain whitelisted domain) fuccess(true)
else
dnsApi.mx(domain).dmap { domains =>
domains.nonEmpty && !domains.exists { disposable(_) }
} >>& checkMail(domain)
}
// the DNS emails should have been preloaded
private[security] val withAcceptableDns = Constraint[String]("constraint.email_acceptable") { e =>
val ok = hasAcceptableDns(EmailAddress(e)).awaitOrElse(
90.millis,
"dns", {
logger.warn(
s"EmailAddressValidator.withAcceptableDns timeout! $e records should have been preloaded"
)
false
}
)
if (ok) Valid
else Invalid(ValidationError("error.email_acceptable"))
}
}
object EmailAddressValidator {
case class Acceptable(acceptable: EmailAddress)
}