emails/validators.py (60 lines of code) (raw):
"""Field validators for emails models."""
import re
from django.contrib.auth.models import User
from privaterelay.utils import flag_is_active_in_task
from .apps import BadWords, emails_config
from .exceptions import (
AccountIsInactiveException,
AccountIsPausedException,
DomainAddrFreeTierException,
DomainAddrNeedSubdomainException,
RelayAddrFreeTierLimitException,
)
# A valid local / username part of an email address:
# can't start or end with a hyphen
# must be 1-63 lowercase alphanumeric characters and/or hyphens
_re_valid_address = re.compile("^(?![-.])[a-z0-9-.]{1,63}(?<![-.])$")
def badwords() -> BadWords:
"""Allow mocking of badwords in tests."""
return emails_config().badwords
def has_bad_words(value: str) -> bool:
"""Return True if the value is a short bad word or contains a long bad word."""
if len(value) <= 4:
return value in badwords().short
return any(badword in value for badword in badwords().long)
def blocklist() -> set[str]:
"""Allow mocking of blocklist in tests."""
return emails_config().blocklist
def is_blocklisted(value: str) -> bool:
"""Return True if the value is a blocked word."""
return value in blocklist()
def check_user_can_make_another_address(user: User) -> None:
"""Raise an exception if the user can not make a RelayAddress."""
if not user.is_active:
raise AccountIsInactiveException()
if user.profile.is_flagged:
raise AccountIsPausedException()
# MPP-3021: return early for premium users to avoid at_max_free_aliases DB query
if user.profile.has_premium:
return
if user.profile.at_max_free_aliases:
raise RelayAddrFreeTierLimitException()
def check_user_can_make_domain_address(user: User) -> None:
"""Raise an exception if the user can not make a DomainAddress."""
if not user.profile.has_premium:
raise DomainAddrFreeTierException()
if not user.profile.subdomain:
raise DomainAddrNeedSubdomainException()
if not user.is_active:
raise AccountIsInactiveException()
if user.profile.is_flagged:
raise AccountIsPausedException()
def valid_address(address: str, domain: str, subdomain: str | None = None) -> bool:
"""Return if the given address parts make a valid Relay email."""
from .models import DeletedAddress, address_hash
address_pattern_valid = valid_address_pattern(address)
address_contains_badword = has_bad_words(address)
address_already_deleted = 0
if not subdomain or flag_is_active_in_task(
"custom_domain_management_redesign", None
):
address_already_deleted = DeletedAddress.objects.filter(
address_hash=address_hash(address, domain=domain, subdomain=subdomain)
).count()
if (
address_already_deleted > 0
or address_contains_badword
or not address_pattern_valid
):
return False
return True
def valid_address_pattern(address: str) -> bool:
"""Return if the local/user part of an address is valid."""
return _re_valid_address.match(address) is not None