def redirect_to_email_login_url(email: str) -> HttpResponseRedirect: login_url = reverse("login") redirect_url = add_query_to_redirect_url( login_url, urlencode({ "email": email, "already_registered": 1 })) return HttpResponseRedirect(redirect_url)
def find_account(request: HttpRequest) -> HttpResponse: from zerver.context_processors import common_context url = reverse("find_account") emails: List[str] = [] if request.method == "POST": form = FindMyTeamForm(request.POST) if form.is_valid(): emails = form.cleaned_data["emails"] # Django doesn't support __iexact__in lookup with EmailField, so we have # to use Qs to get around that without needing to do multiple queries. emails_q = Q() for email in emails: emails_q |= Q(delivery_email__iexact=email) for user in UserProfile.objects.filter(emails_q, is_active=True, is_bot=False, realm__deactivated=False): context = common_context(user) context.update(email=user.delivery_email, ) send_email( "zerver/emails/find_team", to_user_ids=[user.id], context=context, from_address=FromAddress.SUPPORT, ) # Note: Show all the emails in the result otherwise this # feature can be used to ascertain which email addresses # are associated with Zulip. data = urllib.parse.urlencode({"emails": ",".join(emails)}) return redirect(add_query_to_redirect_url(url, data)) else: form = FindMyTeamForm() result = request.GET.get("emails") # The below validation is perhaps unnecessary, in that we # shouldn't get able to get here with an invalid email unless # the user hand-edits the URLs. if result: for email in result.split(","): try: validators.validate_email(email) emails.append(email) except ValidationError: pass return render( request, "zerver/find_account.html", context={ "form": form, "current_url": lambda: url, "emails": emails }, )
def start_remote_user_sso(request: HttpRequest) -> HttpResponse: """ The purpose of this endpoint is to provide an initial step in the flow on which we can handle the special behavior for the desktop app. /accounts/login/sso may have Apache intercepting requests to it to do authentication, so we need this additional endpoint. """ query = request.META["QUERY_STRING"] return redirect(add_query_to_redirect_url(reverse(remote_user_sso), query))
def create_response_for_otp_flow(key: str, otp: str, user_profile: UserProfile, encrypted_key_field_name: str) -> HttpResponse: params = { encrypted_key_field_name: otp_encrypt_api_key(key, otp), 'email': user_profile.delivery_email, 'realm': user_profile.realm.uri, } # We can't use HttpResponseRedirect, since it only allows HTTP(S) URLs response = HttpResponse(status=302) response['Location'] = add_query_to_redirect_url('zulip://login', urllib.parse.urlencode(params)) return response
def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum id = "zulip-" + str(random.randint(100000000000, 999999999999)) password = b32encode(secrets.token_bytes(7))[:10].decode() checksum = hashlib.sha1(("create" + "meetingID=" + id + "&moderatorPW=" + password + "&attendeePW=" + password + "a" + settings.BIG_BLUE_BUTTON_SECRET).encode()).hexdigest() url = add_query_to_redirect_url("/calls/bigbluebutton/join", urlencode({ "meeting_id": "\"" + id + "\"", "password": "******"" + password + "\"", "checksum": "\"" + checksum + "\"" })) return json_success({"url": url})
def find_account(request: HttpRequest) -> HttpResponse: from zerver.context_processors import common_context url = reverse('zerver.views.registration.find_account') emails: List[str] = [] if request.method == 'POST': form = FindMyTeamForm(request.POST) if form.is_valid(): emails = form.cleaned_data['emails'] for user in UserProfile.objects.filter(delivery_email__in=emails, is_active=True, is_bot=False, realm__deactivated=False): context = common_context(user) context.update({ 'email': user.delivery_email, }) send_email('zerver/emails/find_team', to_user_ids=[user.id], context=context, from_address=FromAddress.SUPPORT) # Note: Show all the emails in the result otherwise this # feature can be used to ascertain which email addresses # are associated with Zulip. data = urllib.parse.urlencode({'emails': ','.join(emails)}) return redirect(add_query_to_redirect_url(url, data)) else: form = FindMyTeamForm() result = request.GET.get('emails') # The below validation is perhaps unnecessary, in that we # shouldn't get able to get here with an invalid email unless # the user hand-edits the URLs. if result: for email in result.split(','): try: validators.validate_email(email) emails.append(email) except ValidationError: pass return render( request, 'zerver/find_account.html', context={ 'form': form, 'current_url': lambda: url, 'emails': emails }, )
def password_reset(request: HttpRequest) -> HttpResponse: if is_subdomain_root_or_alias(request) and settings.ROOT_DOMAIN_LANDING_PAGE: redirect_url = add_query_to_redirect_url( reverse("realm_redirect"), urlencode({"next": reverse("password_reset")}) ) return HttpResponseRedirect(redirect_url) response = DjangoPasswordResetView.as_view( template_name="zerver/reset.html", form_class=ZulipPasswordResetForm, success_url="/accounts/password/reset/done/", )(request) assert isinstance(response, HttpResponse) return response
def join_bigbluebutton(request: HttpRequest, meeting_id: str = REQ(validator=check_string), password: str = REQ(validator=check_string), checksum: str = REQ(validator=check_string)) -> HttpResponse: if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None: return json_error(_("Big Blue Button is not configured.")) else: try: response = requests.get( add_query_to_redirect_url(settings.BIG_BLUE_BUTTON_URL + "api/create", urlencode({ "meetingID": meeting_id, "moderatorPW": password, "attendeePW": password + "a", "checksum": checksum }))) response.raise_for_status() except requests.RequestException: return json_error(_("Error connecting to the Big Blue Button server.")) payload = ElementTree.fromstring(response.text) if payload.find("messageKey").text == "checksumError": return json_error(_("Error authenticating to the Big Blue Button server.")) if payload.find("returncode").text != "SUCCESS": return json_error(_("Big Blue Button server returned an unexpected error.")) join_params = urlencode( # type: ignore[type-var] # https://github.com/python/typeshed/issues/4234 { "meetingID": meeting_id, "password": password, "fullName": request.user.full_name, }, quote_via=quote, ) checksum = hashlib.sha1(("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()).hexdigest() redirect_url_base = add_query_to_redirect_url(settings.BIG_BLUE_BUTTON_URL + "api/join", join_params) return redirect(add_query_arg_to_redirect_url(redirect_url_base, "checksum=" + checksum))
def create_response_for_otp_flow(key: str, otp: str, user_profile: UserProfile, encrypted_key_field_name: str) -> HttpResponse: realm_uri = user_profile.realm.uri # Check if the mobile URI is overridden in settings, if so, replace it # This block should only apply to the mobile flow, so we if add others, this # needs to be conditional. if realm_uri in settings.REALM_MOBILE_REMAP_URIS: realm_uri = settings.REALM_MOBILE_REMAP_URIS[realm_uri] params = { encrypted_key_field_name: otp_encrypt_api_key(key, otp), 'email': user_profile.delivery_email, 'realm': realm_uri, } # We can't use HttpResponseRedirect, since it only allows HTTP(S) URLs response = HttpResponse(status=302) response['Location'] = add_query_to_redirect_url('zulip://login', urllib.parse.urlencode(params)) return response
def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: # https://docs.bigbluebutton.org/dev/api.html#create for reference on the api calls # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum id = "zulip-" + str(random.randint(100000000000, 999999999999)) password = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(10)) checksum = hashlib.sha1( ("create" + "meetingID=" + id + "&moderatorPW=" + password + "&attendeePW=" + password + "a" + settings.BIG_BLUE_BUTTON_SECRET).encode()).hexdigest() url = add_query_to_redirect_url( "/calls/bigbluebutton/join", urlencode({ "meeting_id": "\"" + id + "\"", "password": "******"" + password + "\"", "checksum": "\"" + checksum + "\"" })) return json_success({"url": url})
def oauth_redirect_to_root( request: HttpRequest, url: str, sso_type: str, is_signup: bool = False, extra_url_params: Dict[str, str] = {}, next: Optional[str] = REQ(default=None), ) -> HttpResponse: main_site_uri = settings.ROOT_DOMAIN_URI + url if settings.SOCIAL_AUTH_SUBDOMAIN is not None and sso_type == "social": main_site_uri = ( settings.EXTERNAL_URI_SCHEME + settings.SOCIAL_AUTH_SUBDOMAIN + "." + settings.EXTERNAL_HOST ) + url params = { "subdomain": get_subdomain(request), "is_signup": "1" if is_signup else "0", } params["multiuse_object_key"] = request.GET.get("multiuse_object_key", "") # mobile_flow_otp is a one-time pad provided by the app that we # can use to encrypt the API key when passing back to the app. mobile_flow_otp = request.GET.get("mobile_flow_otp") desktop_flow_otp = request.GET.get("desktop_flow_otp") validate_otp_params(mobile_flow_otp, desktop_flow_otp) if mobile_flow_otp is not None: params["mobile_flow_otp"] = mobile_flow_otp if desktop_flow_otp is not None: params["desktop_flow_otp"] = desktop_flow_otp if next: params["next"] = next params = {**params, **extra_url_params} return redirect(add_query_to_redirect_url(main_site_uri, urllib.parse.urlencode(params)))
def oauth_redirect_to_root( request: HttpRequest, url: str, sso_type: str, is_signup: bool = False, extra_url_params: Dict[str, str] = {}) -> HttpResponse: main_site_uri = settings.ROOT_DOMAIN_URI + url if settings.SOCIAL_AUTH_SUBDOMAIN is not None and sso_type == 'social': main_site_uri = (settings.EXTERNAL_URI_SCHEME + settings.SOCIAL_AUTH_SUBDOMAIN + "." + settings.EXTERNAL_HOST) + url params = { 'subdomain': get_subdomain(request), 'is_signup': '1' if is_signup else '0', } params['multiuse_object_key'] = request.GET.get('multiuse_object_key', '') # mobile_flow_otp is a one-time pad provided by the app that we # can use to encrypt the API key when passing back to the app. mobile_flow_otp = request.GET.get('mobile_flow_otp') desktop_flow_otp = request.GET.get('desktop_flow_otp') validate_otp_params(mobile_flow_otp, desktop_flow_otp) if mobile_flow_otp is not None: params['mobile_flow_otp'] = mobile_flow_otp if desktop_flow_otp is not None: params['desktop_flow_otp'] = desktop_flow_otp next = request.GET.get('next') if next: params['next'] = next params = {**params, **extra_url_params} return redirect( add_query_to_redirect_url(main_site_uri, urllib.parse.urlencode(params)))
def login_page( request: HttpRequest, next: str = REQ(default="/"), **kwargs: Any, ) -> HttpResponse: # To support previewing the Zulip login pages, we have a special option # that disables the default behavior of redirecting logged-in users to the # logged-in app. is_preview = "preview" in request.GET if settings.TWO_FACTOR_AUTHENTICATION_ENABLED: if request.user and request.user.is_verified(): return HttpResponseRedirect(request.user.realm.uri) elif request.user.is_authenticated and not is_preview: return HttpResponseRedirect(request.user.realm.uri) if is_subdomain_root_or_alias(request) and settings.ROOT_DOMAIN_LANDING_PAGE: redirect_url = reverse("realm_redirect") if request.GET: redirect_url = add_query_to_redirect_url(redirect_url, request.GET.urlencode()) return HttpResponseRedirect(redirect_url) realm = get_realm_from_request(request) if realm and realm.deactivated: return redirect_to_deactivation_notice() extra_context = kwargs.pop("extra_context", {}) extra_context["next"] = next if dev_auth_enabled() and kwargs.get("template_name") == "zerver/development/dev_login.html": from zerver.views.development.dev_login import add_dev_login_context if "new_realm" in request.POST: try: realm = get_realm(request.POST["new_realm"]) except Realm.DoesNotExist: realm = None add_dev_login_context(realm, extra_context) if realm and "new_realm" in request.POST: # If we're switching realms, redirect to that realm, but # only if it actually exists. return HttpResponseRedirect(realm.uri) if "username" in request.POST: extra_context["email"] = request.POST["username"] extra_context.update(login_context(request)) if settings.TWO_FACTOR_AUTHENTICATION_ENABLED: return start_two_factor_auth(request, extra_context=extra_context, **kwargs) try: template_response = DjangoLoginView.as_view( authentication_form=OurAuthenticationForm, extra_context=extra_context, **kwargs )(request) except ZulipLDAPConfigurationError as e: assert len(e.args) > 1 return redirect_to_misconfigured_ldap_notice(request, e.args[1]) if isinstance(template_response, SimpleTemplateResponse): # Only those responses that are rendered using a template have # context_data attribute. This attribute doesn't exist otherwise. It is # added in SimpleTemplateResponse class, which is a derived class of # HttpResponse. See django.template.response.SimpleTemplateResponse, # https://github.com/django/django/blob/master/django/template/response.py#L19. update_login_page_context(request, template_response.context_data) return template_response
def accounts_register(request: HttpRequest) -> HttpResponse: try: key = request.POST.get("key", default="") confirmation = Confirmation.objects.get(confirmation_key=key) except Confirmation.DoesNotExist: return render(request, "zerver/confirmation_link_expired_error.html") prereg_user = confirmation.content_object if prereg_user.status == confirmation_settings.STATUS_REVOKED: return render(request, "zerver/confirmation_link_expired_error.html") email = prereg_user.email realm_creation = prereg_user.realm_creation password_required = prereg_user.password_required role = prereg_user.invited_as if realm_creation: role = UserProfile.ROLE_REALM_OWNER try: validators.validate_email(email) except ValidationError: return render(request, "zerver/invalid_email.html", context={"invalid_email": True}) if realm_creation: # For creating a new realm, there is no existing realm or domain realm = None else: if get_subdomain(request) != prereg_user.realm.string_id: return render_confirmation_key_error( request, ConfirmationKeyException( ConfirmationKeyException.DOES_NOT_EXIST)) realm = prereg_user.realm try: email_allowed_for_realm(email, realm) except DomainNotAllowedForRealmError: return render( request, "zerver/invalid_email.html", context={ "realm_name": realm.name, "closed_domain": True }, ) except DisposableEmailError: return render( request, "zerver/invalid_email.html", context={ "realm_name": realm.name, "disposable_emails_not_allowed": True }, ) except EmailContainsPlusError: return render( request, "zerver/invalid_email.html", context={ "realm_name": realm.name, "email_contains_plus": True }, ) if realm.deactivated: # The user is trying to register for a deactivated realm. Advise them to # contact support. return redirect_to_deactivation_notice() try: validate_email_not_already_in_realm(realm, email) except ValidationError: return redirect_to_email_login_url(email) name_validated = False full_name = None require_ldap_password = False if request.POST.get("from_confirmation"): try: del request.session["authenticated_full_name"] except KeyError: pass ldap_full_name = None if settings.POPULATE_PROFILE_VIA_LDAP: # If the user can be found in LDAP, we'll take the full name from the directory, # and further down create a form pre-filled with it. for backend in get_backends(): if isinstance(backend, LDAPBackend): try: ldap_username = backend.django_to_ldap_username(email) except ZulipLDAPExceptionNoMatchingLDAPUser: logging.warning( "New account email %s could not be found in LDAP", email) break # Note that this `ldap_user` object is not a # `ZulipLDAPUser` with a `Realm` attached, so # calling `.populate_user()` on it will crash. # This is OK, since we're just accessing this user # to extract its name. # # TODO: We should potentially be accessing this # user to sync its initial avatar and custom # profile fields as well, if we indeed end up # creating a user account through this flow, # rather than waiting until `manage.py # sync_ldap_user_data` runs to populate it. ldap_user = _LDAPUser(backend, ldap_username) try: ldap_full_name = backend.get_mapped_name(ldap_user) except TypeError: break # Check whether this is ZulipLDAPAuthBackend, # which is responsible for authentication and # requires that LDAP accounts enter their LDAP # password to register, or ZulipLDAPUserPopulator, # which just populates UserProfile fields (no auth). require_ldap_password = isinstance(backend, ZulipLDAPAuthBackend) break if ldap_full_name: # We don't use initial= here, because if the form is # complete (that is, no additional fields need to be # filled out by the user) we want the form to validate, # so they can be directly registered without having to # go through this interstitial. form = RegistrationForm({"full_name": ldap_full_name}, realm_creation=realm_creation) request.session["authenticated_full_name"] = ldap_full_name name_validated = True elif realm is not None and realm.is_zephyr_mirror_realm: # For MIT users, we can get an authoritative name from Hesiod. # Technically we should check that this is actually an MIT # realm, but we can cross that bridge if we ever get a non-MIT # zephyr mirroring realm. hesiod_name = compute_mit_user_fullname(email) form = RegistrationForm( initial={ "full_name": hesiod_name if "@" not in hesiod_name else "" }, realm_creation=realm_creation, ) name_validated = True elif prereg_user.full_name: if prereg_user.full_name_validated: request.session[ "authenticated_full_name"] = prereg_user.full_name name_validated = True form = RegistrationForm({"full_name": prereg_user.full_name}, realm_creation=realm_creation) else: form = RegistrationForm( initial={"full_name": prereg_user.full_name}, realm_creation=realm_creation) elif "full_name" in request.POST: form = RegistrationForm( initial={"full_name": request.POST.get("full_name")}, realm_creation=realm_creation, ) else: form = RegistrationForm(realm_creation=realm_creation) else: postdata = request.POST.copy() if name_changes_disabled(realm): # If we populate profile information via LDAP and we have a # verified name from you on file, use that. Otherwise, fall # back to the full name in the request. try: postdata.update( full_name=request.session["authenticated_full_name"]) name_validated = True except KeyError: pass form = RegistrationForm(postdata, realm_creation=realm_creation) if not (password_auth_enabled(realm) and password_required): form["password"].field.required = False if form.is_valid(): if password_auth_enabled(realm) and form["password"].field.required: password = form.cleaned_data["password"] else: # If the user wasn't prompted for a password when # completing the authentication form (because they're # signing up with SSO and no password is required), set # the password field to `None` (Which causes Django to # create an unusable password). password = None if realm_creation: string_id = form.cleaned_data["realm_subdomain"] realm_name = form.cleaned_data["realm_name"] realm = do_create_realm(string_id, realm_name) setup_realm_internal_bots(realm) assert realm is not None full_name = form.cleaned_data["full_name"] default_stream_group_names = request.POST.getlist( "default_stream_group") default_stream_groups = lookup_default_stream_groups( default_stream_group_names, realm) timezone = "" if "timezone" in request.POST and request.POST[ "timezone"] in pytz.all_timezones_set: timezone = request.POST["timezone"] if "source_realm_id" in request.POST: # Non-integer realm_id values like "string" are treated # like the "Do not import" value of "". try: source_realm_id = int(request.POST["source_realm_id"]) except ValueError: source_profile: Optional[UserProfile] = None else: source_profile = get_source_profile(email, source_realm_id) else: source_profile = None if not realm_creation: try: existing_user_profile: Optional[ UserProfile] = get_user_by_delivery_email(email, realm) except UserProfile.DoesNotExist: existing_user_profile = None else: existing_user_profile = None user_profile: Optional[UserProfile] = None return_data: Dict[str, bool] = {} if ldap_auth_enabled(realm): # If the user was authenticated using an external SSO # mechanism like Google or GitHub auth, then authentication # will have already been done before creating the # PreregistrationUser object with password_required=False, and # so we don't need to worry about passwords. # # If instead the realm is using EmailAuthBackend, we will # set their password above. # # But if the realm is using LDAPAuthBackend, we need to verify # their LDAP password (which will, as a side effect, create # the user account) here using authenticate. # pregeg_user.realm_creation carries the information about whether # we're in realm creation mode, and the ldap flow will handle # that and create the user with the appropriate parameters. user_profile = authenticate( request=request, username=email, password=password, realm=realm, prereg_user=prereg_user, return_data=return_data, ) if user_profile is None: can_use_different_backend = email_auth_enabled( realm) or any_social_backend_enabled(realm) if settings.LDAP_APPEND_DOMAIN: # In LDAP_APPEND_DOMAIN configurations, we don't allow making a non-LDAP account # if the email matches the ldap domain. can_use_different_backend = can_use_different_backend and ( not email_belongs_to_ldap(realm, email)) if return_data.get( "no_matching_ldap_user") and can_use_different_backend: # If both the LDAP and Email or Social auth backends are # enabled, and there's no matching user in the LDAP # directory then the intent is to create a user in the # realm with their email outside the LDAP organization # (with e.g. a password stored in the Zulip database, # not LDAP). So we fall through and create the new # account. pass else: # TODO: This probably isn't going to give a # user-friendly error message, but it doesn't # particularly matter, because the registration form # is hidden for most users. view_url = reverse("login") query = urlencode({"email": email}) redirect_url = add_query_to_redirect_url(view_url, query) return HttpResponseRedirect(redirect_url) elif not realm_creation: # Since we'll have created a user, we now just log them in. return login_and_go_to_home(request, user_profile) else: # With realm_creation=True, we're going to return further down, # after finishing up the creation process. pass if existing_user_profile is not None and existing_user_profile.is_mirror_dummy: user_profile = existing_user_profile do_activate_user(user_profile, acting_user=user_profile) do_change_password(user_profile, password) do_change_full_name(user_profile, full_name, user_profile) do_set_user_display_setting(user_profile, "timezone", timezone) # TODO: When we clean up the `do_activate_user` code path, # make it respect invited_as_admin / is_realm_admin. if user_profile is None: user_profile = do_create_user( email, password, realm, full_name, prereg_user=prereg_user, role=role, tos_version=settings.TOS_VERSION, timezone=timezone, default_stream_groups=default_stream_groups, source_profile=source_profile, realm_creation=realm_creation, acting_user=None, ) if realm_creation: bulk_add_subscriptions(realm, [realm.signup_notifications_stream], [user_profile], acting_user=None) send_initial_realm_messages(realm) # Because for realm creation, registration happens on the # root domain, we need to log them into the subdomain for # their new realm. return redirect_and_log_into_subdomain( ExternalAuthResult(user_profile=user_profile, data_dict={"is_realm_creation": True})) # This dummy_backend check below confirms the user is # authenticating to the correct subdomain. auth_result = authenticate( username=user_profile.delivery_email, realm=realm, return_data=return_data, use_dummy_backend=True, ) if return_data.get("invalid_subdomain"): # By construction, this should never happen. logging.error( "Subdomain mismatch in registration %s: %s", realm.subdomain, user_profile.delivery_email, ) return redirect("/") return login_and_go_to_home(request, auth_result) return render( request, "zerver/register.html", context={ "form": form, "email": email, "key": key, "full_name": request.session.get("authenticated_full_name", None), "lock_name": name_validated and name_changes_disabled(realm), # password_auth_enabled is normally set via our context processor, # but for the registration form, there is no logged in user yet, so # we have to set it here. "creating_new_team": realm_creation, "password_required": password_auth_enabled(realm) and password_required, "require_ldap_password": require_ldap_password, "password_auth_enabled": password_auth_enabled(realm), "root_domain_available": is_root_domain_available(), "default_stream_groups": [] if realm is None else get_default_stream_groups(realm), "accounts": get_accounts_for_email(email), "MAX_REALM_NAME_LENGTH": str(Realm.MAX_REALM_NAME_LENGTH), "MAX_NAME_LENGTH": str(UserProfile.MAX_NAME_LENGTH), "MAX_PASSWORD_LENGTH": str(form.MAX_PASSWORD_LENGTH), "MAX_REALM_SUBDOMAIN_LENGTH": str(Realm.MAX_REALM_SUBDOMAIN_LENGTH), }, )
def redirect_to_email_login_url(email: str) -> HttpResponseRedirect: login_url = reverse('login') email = urllib.parse.quote_plus(email) redirect_url = add_query_to_redirect_url(login_url, 'already_registered=' + email) return HttpResponseRedirect(redirect_url)
def find_account( request: HttpRequest, raw_emails: Optional[str] = REQ("emails", default=None) ) -> HttpResponse: url = reverse("find_account") emails: List[str] = [] if request.method == "POST": form = FindMyTeamForm(request.POST) if form.is_valid(): emails = form.cleaned_data["emails"] for i in range(len(emails)): try: rate_limit_request_by_ip(request, domain="find_account_by_ip") except RateLimited as e: assert e.secs_to_freedom is not None return render( request, "zerver/rate_limit_exceeded.html", context={"retry_after": int(e.secs_to_freedom)}, status=429, ) # Django doesn't support __iexact__in lookup with EmailField, so we have # to use Qs to get around that without needing to do multiple queries. emails_q = Q() for email in emails: emails_q |= Q(delivery_email__iexact=email) user_profiles = UserProfile.objects.filter( emails_q, is_active=True, is_bot=False, realm__deactivated=False) # We organize the data in preparation for sending exactly # one outgoing email per provided email address, with each # email listing all of the accounts that email address has # with the current Zulip server. context: Dict[str, Dict[str, Any]] = {} for user in user_profiles: key = user.delivery_email.lower() context.setdefault(key, {}) context[key].setdefault("realms", []) context[key]["realms"].append(user.realm) context[key]["external_host"] = settings.EXTERNAL_HOST # This value will end up being the last user ID among # matching accounts; since it's only used for minor # details like language, that arbitrary choice is OK. context[key]["to_user_id"] = user.id for delivery_email, realm_context in context.items(): realm_context["email"] = delivery_email send_email( "zerver/emails/find_team", to_user_ids=[realm_context["to_user_id"]], context=realm_context, from_address=FromAddress.SUPPORT, request=request, ) # Note: Show all the emails in the result otherwise this # feature can be used to ascertain which email addresses # are associated with Zulip. data = urllib.parse.urlencode({"emails": ",".join(emails)}) return redirect(add_query_to_redirect_url(url, data)) else: form = FindMyTeamForm() # The below validation is perhaps unnecessary, in that we # shouldn't get able to get here with an invalid email unless # the user hand-edits the URLs. if raw_emails: for email in raw_emails.split(","): try: validators.validate_email(email) emails.append(email) except ValidationError: pass return render( request, "zerver/find_account.html", context={ "form": form, "current_url": lambda: url, "emails": emails }, )