2022-07-27 23:33:49 +02:00
|
|
|
from email.headerregistry import Address
|
2020-03-02 16:07:19 +01:00
|
|
|
from typing import Callable, Dict, Optional, Set, Tuple
|
2020-03-05 13:54:37 +01:00
|
|
|
|
2020-03-02 22:26:24 +01:00
|
|
|
from django.core import validators
|
|
|
|
from django.core.exceptions import ValidationError
|
2021-04-16 00:57:30 +02:00
|
|
|
from django.utils.translation import gettext as _
|
2020-03-05 13:54:37 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.name_restrictions import is_disposable_domain
|
2020-09-02 05:21:28 +02:00
|
|
|
|
2020-03-05 13:54:37 +01:00
|
|
|
# TODO: Move DisposableEmailError, etc. into here.
|
2023-12-15 02:14:24 +01:00
|
|
|
from zerver.models import Realm, RealmDomain
|
|
|
|
from zerver.models.realms import (
|
2020-03-05 13:54:37 +01:00
|
|
|
DisposableEmailError,
|
|
|
|
DomainNotAllowedForRealmError,
|
|
|
|
EmailContainsPlusError,
|
|
|
|
)
|
2023-12-15 01:16:00 +01:00
|
|
|
from zerver.models.users import get_users_by_delivery_email, is_cross_realm_bot_email
|
2020-03-05 13:54:37 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2020-03-05 13:54:37 +01:00
|
|
|
def validate_disposable(email: str) -> None:
|
2022-07-27 23:33:49 +02:00
|
|
|
if is_disposable_domain(Address(addr_spec=email).domain):
|
2020-03-05 13:54:37 +01:00
|
|
|
raise DisposableEmailError
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-03-05 13:54:37 +01:00
|
|
|
def get_realm_email_validator(realm: Realm) -> Callable[[str], None]:
|
|
|
|
if not realm.emails_restricted_to_domains:
|
2022-02-08 00:13:33 +01:00
|
|
|
# Should we also do '+' check for non-restricted realms?
|
2020-03-05 13:54:37 +01:00
|
|
|
if realm.disallow_disposable_email_addresses:
|
|
|
|
return validate_disposable
|
|
|
|
|
|
|
|
# allow any email through
|
|
|
|
return lambda email: None
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2020-03-05 13:54:37 +01:00
|
|
|
RESTRICTIVE REALMS:
|
|
|
|
|
|
|
|
Some realms only allow emails within a set
|
|
|
|
of domains that are configured in RealmDomain.
|
|
|
|
|
|
|
|
We get the set of domains up front so that
|
|
|
|
folks can validate multiple emails without
|
|
|
|
multiple round trips to the database.
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2020-03-05 13:54:37 +01:00
|
|
|
|
|
|
|
query = RealmDomain.objects.filter(realm=realm)
|
2021-02-12 08:20:45 +01:00
|
|
|
rows = list(query.values("allow_subdomains", "domain"))
|
2020-03-05 13:54:37 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
allowed_domains = {r["domain"] for r in rows}
|
2020-03-05 13:54:37 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
allowed_subdomains = {r["domain"] for r in rows if r["allow_subdomains"]}
|
2020-03-05 13:54:37 +01:00
|
|
|
|
|
|
|
def validate(email: str) -> None:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-05 13:54:37 +01:00
|
|
|
We don't have to do a "disposable" check for restricted
|
|
|
|
domains, since the realm is already giving us
|
|
|
|
a small whitelist.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-05 13:54:37 +01:00
|
|
|
|
2022-07-27 23:33:49 +02:00
|
|
|
address = Address(addr_spec=email)
|
|
|
|
if "+" in address.username:
|
2020-03-05 13:54:37 +01:00
|
|
|
raise EmailContainsPlusError
|
|
|
|
|
2022-10-20 02:01:24 +02:00
|
|
|
domain = address.domain.lower()
|
2020-03-05 13:54:37 +01:00
|
|
|
|
|
|
|
if domain in allowed_domains:
|
|
|
|
return
|
|
|
|
|
|
|
|
while len(domain) > 0:
|
2021-02-12 08:20:45 +01:00
|
|
|
subdomain, sep, domain = domain.partition(".")
|
2020-03-05 13:54:37 +01:00
|
|
|
if domain in allowed_subdomains:
|
|
|
|
return
|
|
|
|
|
|
|
|
raise DomainNotAllowedForRealmError
|
|
|
|
|
|
|
|
return validate
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-03-05 13:54:37 +01:00
|
|
|
# Is a user with the given email address allowed to be in the given realm?
|
|
|
|
# (This function does not check whether the user has been invited to the realm.
|
|
|
|
# So for invite-only realms, this is the test for whether a user can be invited,
|
|
|
|
# not whether the user can sign up currently.)
|
|
|
|
def email_allowed_for_realm(email: str, realm: Realm) -> None:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-05 13:54:37 +01:00
|
|
|
Avoid calling this in a loop!
|
|
|
|
Instead, call get_realm_email_validator()
|
|
|
|
outside of the loop.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-05 13:54:37 +01:00
|
|
|
get_realm_email_validator(realm)(email)
|
2020-03-02 22:26:24 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-03-02 22:26:24 +01:00
|
|
|
def validate_email_is_valid(
|
|
|
|
email: str,
|
|
|
|
validate_email_allowed_in_realm: Callable[[str], None],
|
|
|
|
) -> Optional[str]:
|
|
|
|
try:
|
|
|
|
validators.validate_email(email)
|
|
|
|
except ValidationError:
|
|
|
|
return _("Invalid address.")
|
|
|
|
|
|
|
|
try:
|
|
|
|
validate_email_allowed_in_realm(email)
|
|
|
|
except DomainNotAllowedForRealmError:
|
|
|
|
return _("Outside your domain.")
|
|
|
|
except DisposableEmailError:
|
|
|
|
return _("Please use your real email address.")
|
|
|
|
except EmailContainsPlusError:
|
|
|
|
return _("Email addresses containing + are not allowed.")
|
|
|
|
|
|
|
|
return None
|
2020-03-02 16:07:19 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-03-02 16:07:19 +01:00
|
|
|
def email_reserved_for_system_bots_error(email: str) -> str:
|
2021-02-12 08:20:45 +01:00
|
|
|
return f"{email} is reserved for system bots"
|
2020-03-02 16:07:19 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2020-03-02 16:07:19 +01:00
|
|
|
def get_existing_user_errors(
|
|
|
|
target_realm: Realm,
|
|
|
|
emails: Set[str],
|
2021-02-12 08:19:30 +01:00
|
|
|
verbose: bool = False,
|
2020-03-05 17:24:47 +01:00
|
|
|
) -> Dict[str, Tuple[str, bool]]:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-02 16:07:19 +01:00
|
|
|
We use this function even for a list of one emails.
|
|
|
|
|
|
|
|
It checks "new" emails to make sure that they don't
|
|
|
|
already exist. There's a bit of fiddly logic related
|
|
|
|
to cross-realm bots and mirror dummies too.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-02 18:56:52 +01:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
errors: Dict[str, Tuple[str, bool]] = {}
|
2020-03-02 16:07:19 +01:00
|
|
|
|
2020-03-02 18:56:52 +01:00
|
|
|
users = get_users_by_delivery_email(emails, target_realm).only(
|
2021-02-12 08:20:45 +01:00
|
|
|
"delivery_email",
|
|
|
|
"is_active",
|
|
|
|
"is_mirror_dummy",
|
2020-03-02 18:56:52 +01:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2020-03-02 18:56:52 +01:00
|
|
|
A note on casing: We will preserve the casing used by
|
|
|
|
the user for email in most of this code. The only
|
|
|
|
exception is when we do existence checks against
|
|
|
|
the `user_dict` dictionary. (We don't allow two
|
|
|
|
users in the same realm to have the same effective
|
|
|
|
delivery email.)
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2020-03-12 14:54:49 +01:00
|
|
|
user_dict = {user.delivery_email.lower(): user for user in users}
|
2020-03-02 18:56:52 +01:00
|
|
|
|
2020-03-02 16:07:19 +01:00
|
|
|
def process_email(email: str) -> None:
|
|
|
|
if is_cross_realm_bot_email(email):
|
2020-03-05 17:24:47 +01:00
|
|
|
if verbose:
|
|
|
|
msg = email_reserved_for_system_bots_error(email)
|
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
msg = _("Reserved for system bots.")
|
2020-03-02 16:07:19 +01:00
|
|
|
deactivated = False
|
2020-03-05 17:24:47 +01:00
|
|
|
errors[email] = (msg, deactivated)
|
2020-03-02 16:07:19 +01:00
|
|
|
return
|
|
|
|
|
2020-03-02 18:56:52 +01:00
|
|
|
existing_user_profile = user_dict.get(email.lower())
|
|
|
|
|
|
|
|
if existing_user_profile is None:
|
2020-03-02 16:07:19 +01:00
|
|
|
# HAPPY PATH! Most people invite users that don't exist yet.
|
|
|
|
return
|
|
|
|
|
|
|
|
if existing_user_profile.is_mirror_dummy:
|
|
|
|
if existing_user_profile.is_active:
|
|
|
|
raise AssertionError("Mirror dummy user is already active!")
|
|
|
|
return
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2020-03-02 16:07:19 +01:00
|
|
|
Email has already been taken by a "normal" user.
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2020-03-02 16:07:19 +01:00
|
|
|
deactivated = not existing_user_profile.is_active
|
|
|
|
|
|
|
|
if existing_user_profile.is_active:
|
2020-03-05 17:24:47 +01:00
|
|
|
if verbose:
|
2021-02-12 08:20:45 +01:00
|
|
|
msg = _("{email} already has an account").format(email=email)
|
2020-03-05 17:24:47 +01:00
|
|
|
else:
|
|
|
|
msg = _("Already has an account.")
|
2020-03-02 16:07:19 +01:00
|
|
|
else:
|
2020-03-05 17:24:47 +01:00
|
|
|
msg = _("Account has been deactivated.")
|
2020-03-02 16:07:19 +01:00
|
|
|
|
2020-03-05 17:24:47 +01:00
|
|
|
errors[email] = (msg, deactivated)
|
2020-03-02 16:07:19 +01:00
|
|
|
|
|
|
|
for email in emails:
|
|
|
|
process_email(email)
|
|
|
|
|
|
|
|
return errors
|
2020-03-05 16:56:08 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def validate_email_not_already_in_realm(
|
|
|
|
target_realm: Realm, email: str, verbose: bool = True
|
|
|
|
) -> None:
|
|
|
|
"""
|
2020-03-05 16:56:08 +01:00
|
|
|
NOTE:
|
|
|
|
Only use this to validate that a single email
|
|
|
|
is not already used in the realm.
|
|
|
|
|
|
|
|
We should start using bulk_check_new_emails()
|
|
|
|
for any endpoint that takes multiple emails,
|
|
|
|
such as the "invite" interface.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-05 17:24:47 +01:00
|
|
|
error_dict = get_existing_user_errors(target_realm, {email}, verbose)
|
2020-03-05 16:56:08 +01:00
|
|
|
|
|
|
|
# Loop through errors, the only key should be our email.
|
|
|
|
for key, error_info in error_dict.items():
|
|
|
|
assert key == email
|
2020-03-05 17:24:47 +01:00
|
|
|
msg, deactivated = error_info
|
|
|
|
raise ValidationError(msg)
|