diff --git a/templates/zerver/closed_realm.html b/templates/zerver/closed_realm.html deleted file mode 100644 index 678d2bcc4a..0000000000 --- a/templates/zerver/closed_realm.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "zerver/portico.html" %} -{% block portico_content %} - -

{{ _('Private organization') }}

- -

{{ _('Hi there! Thank you for your interest in Zulip.') }}

- -

{% trans %}The organization you are trying to join, {{ closed_domain_name }}, only allows users with e-mail addresses within the organization. Please ask for a new invite to an appropriate e-mail address.{% endtrans %}

- -{% endblock %} diff --git a/templates/zerver/invalid_email.html b/templates/zerver/invalid_email.html new file mode 100644 index 0000000000..953d2a9b8a --- /dev/null +++ b/templates/zerver/invalid_email.html @@ -0,0 +1,27 @@ +{% extends "zerver/portico.html" %} +{% block portico_content %} + +

{{ _('Invalid email') }}

+ +

{{ _('Hi there! Thank you for your interest in Zulip.') }}

+ + {% if closed_domain %} +

+ {% trans %} + The organization you are trying to join, {{ realm_name }}, + only allows users with email addresses within the + organization. Please sign up using appropriate email address. + {% endtrans %} +

+ {% endif %} + + {% if disposable_emails_not_allowed %} +

+ {% trans %}The organization you are trying to join, + {{realm_name}}, does not allow signups using disposable email + addresses. Please sign up using a real email address. + {% endtrans %} +

+ {% endif %} + +{% endblock %} diff --git a/zerver/forms.py b/zerver/forms.py index a774cc24ff..569bea19dd 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -24,8 +24,8 @@ from zerver.lib.request import JsonableError from zerver.lib.send_email import send_email, FromAddress from zerver.lib.subdomains import get_subdomain, user_matches_subdomain, is_root_domain_available from zerver.lib.users import check_full_name -from zerver.models import Realm, get_user, UserProfile, \ - get_realm, email_to_domain, email_allowed_for_realm +from zerver.models import Realm, get_user, UserProfile, get_realm, email_to_domain, \ + email_allowed_for_realm, disposable_email_check from zproject.backends import email_auth_enabled import logging @@ -151,6 +151,7 @@ class HomepageForm(forms.Form): "that are allowed to register for accounts in this organization.").format( string_id=realm.string_id, email=email)) + disposable_email_check(realm, email) validate_email_for_realm(realm, email) if realm.is_zephyr_mirror_realm: diff --git a/zerver/migrations/0147_realm_disallow_disposable_email_addresses.py b/zerver/migrations/0147_realm_disallow_disposable_email_addresses.py new file mode 100644 index 0000000000..78a7d2425f --- /dev/null +++ b/zerver/migrations/0147_realm_disallow_disposable_email_addresses.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2018-03-05 19:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0146_userprofile_message_content_in_email_notifications'), + ] + + operations = [ + migrations.AddField( + model_name='realm', + name='disallow_disposable_email_addresses', + field=models.BooleanField(default=True), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 95b5449c94..8c3a8c1988 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -33,6 +33,8 @@ from django.utils.translation import ugettext_lazy as _ from zerver.lib import cache from zerver.lib.validator import check_int, check_float, check_string, \ check_short_string +from zerver.lib.name_restrictions import is_disposable_domain + from django.utils.encoding import force_text from bitfield import BitField @@ -151,6 +153,7 @@ class Realm(models.Model): show_digest_email = models.BooleanField(default=True) # type: bool name_changes_disabled = models.BooleanField(default=False) # type: bool email_changes_disabled = models.BooleanField(default=False) # type: bool + disallow_disposable_email_addresses = models.BooleanField(default=True) # type: bool description = models.TextField(null=True) # type: Optional[Text] send_welcome_emails = models.BooleanField(default=True) # type: bool @@ -196,6 +199,7 @@ class Realm(models.Model): create_stream_by_admins_only=bool, default_language=Text, description=Text, + disallow_disposable_email_addresses=bool, email_changes_disabled=bool, invite_required=bool, invite_by_admins_only=bool, @@ -386,6 +390,11 @@ def email_allowed_for_realm(email: Text, realm: Realm) -> bool: return True return False +def disposable_email_check(realm: Realm, email: Text) -> None: + if not realm.restricted_to_domain and realm.disallow_disposable_email_addresses: + if is_disposable_domain(email_to_domain(email)): + raise ValidationError("Please use your real email address.") + def get_realm_domains(realm: Realm) -> List[Dict[str, Text]]: return list(realm.realmdomain_set.values('domain', 'allow_subdomains')) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 13a21c5a23..1509322b7c 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -118,6 +118,7 @@ class HomeTest(ZulipTestCase): "realm_default_stream_groups", "realm_default_streams", "realm_description", + "realm_disallow_disposable_email_addresses", "realm_domains", "realm_email_auth_enabled", "realm_email_changes_disabled", diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index cd8620f748..ead1fb6b49 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -791,7 +791,32 @@ so we didn't send them an invitation. We did send invitations to everyone else!" result = self.submit_reg_form_for_user("foo@example.com", "password") self.assertEqual(result.status_code, 200) - self.assert_in_response("only allows users with e-mail", result) + self.assert_in_response("only allows users with email addresses", result) + + def test_disposable_emails_before_closing(self) -> None: + """ + If you invite someone with a disposable email when + `disallow_disposable_email_addresses = False`, but + later changes to true, the invitation should succeed + but the invitee's signup attempt should fail. + """ + zulip_realm = get_realm("zulip") + zulip_realm.restricted_to_domain = False + zulip_realm.disallow_disposable_email_addresses = False + zulip_realm.save() + + self.login(self.example_email("hamlet")) + external_address = "foo@mailnator.com" + + self.assert_json_success(self.invite(external_address, ["Denmark"])) + self.check_sent_emails([external_address]) + + zulip_realm.disallow_disposable_email_addresses = True + zulip_realm.save() + + result = self.submit_reg_form_for_user("foo@mailnator.com", "password") + self.assertEqual(result.status_code, 200) + self.assert_in_response("Please sign up using a real email address.", result) def test_invite_with_non_ascii_streams(self) -> None: """ @@ -1841,6 +1866,18 @@ class UserSignUpTest(ZulipTestCase): self.assertIn("Your email address, {}, is not in one of the domains".format(email), form.errors['email'][0]) + def test_failed_signup_due_to_disposable_email(self) -> None: + realm = get_realm('zulip') + realm.restricted_to_domain = False + realm.disallow_disposable_email_addresses = True + realm.save() + + request = HostRequestMock(host = realm.host) + request.session = {} # type: ignore + email = 'abc@mailnator.com' + form = HomepageForm({'email': email}, realm=realm) + self.assertIn("Please use your real email address", form.errors['email'][0]) + def test_failed_signup_due_to_invite_required(self) -> None: realm = get_realm('zulip') realm.invite_required = True diff --git a/zerver/tests/test_templates.py b/zerver/tests/test_templates.py index 4d74e70b79..c7d5fd425f 100644 --- a/zerver/tests/test_templates.py +++ b/zerver/tests/test_templates.py @@ -105,7 +105,7 @@ class TemplateTestCase(ZulipTestCase): 'zilencer/enterprise_tos_accept_body.txt', 'zerver/zulipchat_migration_tos.html', 'zilencer/enterprise_tos_accept_body.txt', - 'zerver/closed_realm.html', + 'zerver/invalid_email.html', 'zerver/topic_is_muted.html', 'zerver/bankruptcy.html', 'zerver/lightbox_overlay.html', diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index 7de0024e5d..437f6f31bc 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -181,6 +181,7 @@ class AdminCreateUserTest(ZulipTestCase): admin = self.example_user('hamlet') admin_email = admin.email + realm = admin.realm self.login(admin_email) do_change_is_admin(admin, True) @@ -223,8 +224,6 @@ class AdminCreateUserTest(ZulipTestCase): "Email 'romeo@not-zulip.com' not allowed in this organization") RealmDomain.objects.create(realm=get_realm('zulip'), domain='zulip.net') - - # HAPPY PATH STARTS HERE valid_params = dict( email='romeo@zulip.net', password='xxxx', @@ -239,12 +238,20 @@ class AdminCreateUserTest(ZulipTestCase): self.assertEqual(new_user.full_name, 'Romeo Montague') self.assertEqual(new_user.short_name, 'Romeo') - # One more error condition to test--we can't create - # the same user twice. + # we can't create the same user twice. result = self.client_post("/json/users", valid_params) self.assert_json_error(result, "Email 'romeo@zulip.net' already in use") + # Don't allow user to sign up with disposable email. + realm.restricted_to_domain = False + realm.disallow_disposable_email_addresses = True + realm.save() + + valid_params["email"] = "abc@mailnator.com" + result = self.client_post("/json/users", valid_params) + self.assert_json_error(result, "Disposable emails are not allowed for realm 'zulip'") + class UserProfileTest(ZulipTestCase): def test_get_emails_from_user_ids(self) -> None: hamlet = self.example_user('hamlet') diff --git a/zerver/views/realm.py b/zerver/views/realm.py index df09a3fd95..1be0363320 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -30,6 +30,7 @@ def update_realm( name: Optional[str]=REQ(validator=check_string, default=None), description: Optional[str]=REQ(validator=check_string, default=None), restricted_to_domain: Optional[bool]=REQ(validator=check_bool, default=None), + disallow_disposable_email_addresses: Optional[bool]=REQ(validator=check_bool, default=None), invite_required: Optional[bool]=REQ(validator=check_bool, default=None), invite_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None), name_changes_disabled: Optional[bool]=REQ(validator=check_bool, default=None), diff --git a/zerver/views/registration.py b/zerver/views/registration.py index 7dbcaa6530..67e32e19e2 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -14,7 +14,7 @@ from django.core import validators from zerver.context_processors import get_realm_from_request from zerver.models import UserProfile, Realm, Stream, MultiuseInvite, \ name_changes_disabled, email_to_username, email_allowed_for_realm, \ - get_realm, get_user, get_default_stream_groups + get_realm, get_user, get_default_stream_groups, disposable_email_check from zerver.lib.send_email import send_email, FromAddress from zerver.lib.events import do_events_register from zerver.lib.actions import do_change_password, do_change_full_name, do_change_is_admin, \ @@ -88,8 +88,14 @@ def accounts_register(request: HttpRequest) -> HttpResponse: request, ConfirmationKeyException(ConfirmationKeyException.DOES_NOT_EXIST)) if not email_allowed_for_realm(email, realm): - return render(request, "zerver/closed_realm.html", - context={"closed_domain_name": realm.name}) + return render(request, "zerver/invalid_email.html", + context={"realm_name": realm.name, "closed_domain": True}) + + try: + disposable_email_check(realm, email) + except ValidationError: + return render(request, "zerver/invalid_email.html", + context={"realm_name": realm.name, "disposable_emails_not_allowed": True}) if realm.deactivated: # The user is trying to register for a deactivated realm. Advise them to diff --git a/zerver/views/users.py b/zerver/views/users.py index 61ff0a4887..fe335ebc1f 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse from django.utils.translation import ugettext as _ from django.shortcuts import redirect, render from django.conf import settings +from django.core.exceptions import ValidationError from zerver.decorator import require_realm_admin, zulip_login_required from zerver.forms import CreateUserForm @@ -30,7 +31,8 @@ from zerver.lib.users import check_valid_bot_type, check_bot_creation_policy, \ check_full_name, check_short_name, check_valid_interface_type, check_valid_bot_config from zerver.lib.utils import generate_random_token from zerver.models import UserProfile, Stream, Message, email_allowed_for_realm, \ - get_user_profile_by_id, get_user, Service, get_user_including_cross_realm + get_user_profile_by_id, get_user, Service, get_user_including_cross_realm, \ + disposable_email_check from zerver.lib.create_user import random_api_key @@ -460,6 +462,11 @@ def create_user_backend(request: HttpRequest, user_profile: UserProfile, return json_error(_("Email '%(email)s' not allowed in this organization") % {'email': email}) + try: + disposable_email_check(realm, email) + except ValidationError: + return json_error(_("Disposable emails are not allowed for realm '{}'".format(realm.string_id))) + try: get_user(email, user_profile.realm) return json_error(_("Email '%s' already in use") % (email,))