• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / 98c45938-8d23-4015-90f8-7dff916ce789

21 Jun 2024 01:50PM CUT coverage: 85.357% (+0.02%) from 85.337%
98c45938-8d23-4015-90f8-7dff916ce789

push

circleci

web-flow
Merge pull request #4799 from mozilla/split-exceptions-validators-mpp-3827

MPP-3827: Move non-model code out of `emails/models.py`

4004 of 5135 branches covered (77.97%)

Branch coverage included in aggregate %.

335 of 339 new or added lines in 9 files covered. (98.82%)

1 existing line in 1 file now uncovered.

15746 of 18003 relevant lines covered (87.46%)

10.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.94
/emails/validators.py
1
"""Field validators for emails models."""
2

3
import re
1✔
4
from typing import Any
1✔
5

6
from django.contrib.auth.models import User
1✔
7

8
from privaterelay.utils import flag_is_active_in_task
1✔
9

10
from .apps import emails_config
1✔
11
from .exceptions import (
1✔
12
    AccountIsInactiveException,
13
    AccountIsPausedException,
14
    CannotMakeSubdomainException,
15
    DomainAddrFreeTierException,
16
    DomainAddrNeedSubdomainException,
17
    RelayAddrFreeTierLimitException,
18
)
19

20
# A valid local / username part of an email address:
21
#   can't start or end with a hyphen
22
#   must be 1-63 lowercase alphanumeric characters and/or hyphens
23
_re_valid_address = re.compile("^(?![-.])[a-z0-9-.]{1,63}(?<![-.])$")
1✔
24

25
# A valid subdomain:
26
#   can't start or end with a hyphen
27
#   must be 1-63 alphanumeric characters and/or hyphens
28
_re_valid_subdomain = re.compile("^(?!-)[a-z0-9-]{1,63}(?<!-)$")
1✔
29

30

31
def badwords() -> list[str]:
1✔
32
    """Allow mocking of badwords in tests."""
33
    return emails_config().badwords
1✔
34

35

36
def has_bad_words(value: str) -> bool:
1✔
37
    """Return True if the value is a short bad word or contains a long bad word."""
38
    for badword in badwords():
1✔
39
        badword = badword.strip()
1✔
40
        if len(badword) <= 4 and badword == value:
1✔
41
            return True
1✔
42
        if len(badword) > 4 and badword in value:
1✔
43
            return True
1✔
44
    return False
1✔
45

46

47
def blocklist() -> list[str]:
1✔
48
    """Allow mocking of blocklist in tests."""
49
    return emails_config().blocklist
1✔
50

51

52
def is_blocklisted(value: str) -> bool:
1✔
53
    """Return True if the value is a blocked word."""
54
    return any(blockedword == value for blockedword in blocklist())
1✔
55

56

57
def check_user_can_make_another_address(user: User) -> None:
1✔
58
    """Raise an exception if the user can not make a RelayAddress."""
59
    if not user.is_active:
1✔
60
        raise AccountIsInactiveException()
1✔
61

62
    if user.profile.is_flagged:
1✔
63
        raise AccountIsPausedException()
1✔
64
    # MPP-3021: return early for premium users to avoid at_max_free_aliases DB query
65
    if user.profile.has_premium:
1✔
66
        return
1✔
67
    if user.profile.at_max_free_aliases:
1✔
68
        raise RelayAddrFreeTierLimitException()
1✔
69

70

71
def check_user_can_make_domain_address(user: User) -> None:
1✔
72
    """Raise an exception if the user can not make a DomainAddress."""
73
    if not user.profile.has_premium:
1✔
74
        raise DomainAddrFreeTierException()
1✔
75

76
    if not user.profile.subdomain:
1✔
77
        raise DomainAddrNeedSubdomainException()
1✔
78

79
    if not user.is_active:
1!
NEW
80
        raise AccountIsInactiveException()
×
81

82
    if user.profile.is_flagged:
1✔
83
        raise AccountIsPausedException()
1✔
84

85

86
def valid_address(address: str, domain: str, subdomain: str | None = None) -> bool:
1✔
87
    """Return if the given address parts make a valid Relay email."""
88
    from .models import DeletedAddress, address_hash
1✔
89

90
    address_pattern_valid = valid_address_pattern(address)
1✔
91
    address_contains_badword = has_bad_words(address)
1✔
92
    address_already_deleted = 0
1✔
93
    if not subdomain or flag_is_active_in_task(
1✔
94
        "custom_domain_management_redesign", None
95
    ):
96
        address_already_deleted = DeletedAddress.objects.filter(
1✔
97
            address_hash=address_hash(address, domain=domain, subdomain=subdomain)
98
        ).count()
99
    if (
1✔
100
        address_already_deleted > 0
101
        or address_contains_badword
102
        or not address_pattern_valid
103
    ):
104
        return False
1✔
105
    return True
1✔
106

107

108
def valid_address_pattern(address: str) -> bool:
1✔
109
    """Return if the local/user part of an address is valid."""
110
    return _re_valid_address.match(address) is not None
1✔
111

112

113
def valid_available_subdomain(subdomain: Any) -> None:
1✔
114
    """Raise CannotMakeSubdomainException if the subdomain fails a validation test."""
115
    from .models import RegisteredSubdomain, hash_subdomain
1✔
116

117
    if not subdomain:
1✔
118
        raise CannotMakeSubdomainException("error-subdomain-cannot-be-empty-or-null")
1✔
119
    subdomain = str(subdomain).lower()
1✔
120

121
    # valid subdomains:
122
    #   have to meet the rules for length and characters
123
    valid = _re_valid_subdomain.match(subdomain) is not None
1✔
124
    #   can't have "bad" words in them
125
    bad_word = has_bad_words(subdomain)
1✔
126
    #   can't have "blocked" words in them
127
    blocked_word = is_blocklisted(subdomain)
1✔
128
    #   can't be taken by someone else
129
    taken = (
1✔
130
        RegisteredSubdomain.objects.filter(
131
            subdomain_hash=hash_subdomain(subdomain)
132
        ).count()
133
        > 0
134
    )
135
    if not valid or bad_word or blocked_word or taken:
1✔
136
        raise CannotMakeSubdomainException("error-subdomain-not-available")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc