Пример #1
0
def confirm_email_change(request: HttpRequest,
                         confirmation_key: str) -> HttpResponse:
    try:
        email_change_object = get_object_from_key(confirmation_key,
                                                  Confirmation.EMAIL_CHANGE)
    except ConfirmationKeyException as exception:
        return render_confirmation_key_error(request, exception)

    new_email = email_change_object.new_email
    old_email = email_change_object.old_email
    user_profile = email_change_object.user_profile

    if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin:
        raise JsonableError(
            _("Email address changes are disabled in this organization."))

    do_change_user_delivery_email(user_profile, new_email)

    context = {'realm_name': user_profile.realm.name, 'new_email': new_email}
    language = user_profile.default_language
    send_email('zerver/emails/notify_change_in_email',
               to_emails=[old_email],
               from_name=FromAddress.security_email_from_name(
                   user_profile=user_profile),
               from_address=FromAddress.SUPPORT,
               language=language,
               context=context)

    ctx = {
        'new_email': new_email,
        'old_email': old_email,
    }
    return render(request,
                  'confirmation/confirm_email_change.html',
                  context=ctx)
Пример #2
0
def downgrade_small_realms_behind_on_payments_as_needed() -> None:
    customers = Customer.objects.all().exclude(stripe_customer_id=None)
    for customer in customers:
        realm = customer.realm

        # For larger realms, we generally want to talk to the customer
        # before downgrading or cancelling invoices; so this logic only applies with 5.
        if get_latest_seat_count(realm) >= 5:
            continue

        if get_current_plan_by_customer(customer) is not None:
            # Only customers with last 2 invoices open should be downgraded.
            if not customer_has_last_n_invoices_open(customer, 2):
                continue

            # We've now decided to downgrade this customer and void all invoices, and the below will execute this.

            downgrade_now_without_creating_additional_invoices(realm)
            void_all_open_invoices(realm)
            context: Dict[str, Union[str, Realm]] = {
                "upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}",
                "realm": realm,
            }
            send_email_to_billing_admins_and_realm_owners(
                "zerver/emails/realm_auto_downgraded",
                realm,
                from_name=FromAddress.security_email_from_name(language=realm.default_language),
                from_address=FromAddress.tokenized_no_reply_address(),
                language=realm.default_language,
                context=context,
            )
        else:
            if customer_has_last_n_invoices_open(customer, 1):
                void_all_open_invoices(realm)
Пример #3
0
def do_start_email_change_process(user_profile: UserProfile,
                                  new_email: str) -> None:
    old_email = user_profile.delivery_email
    obj = EmailChangeStatus.objects.create(
        new_email=new_email,
        old_email=old_email,
        user_profile=user_profile,
        realm=user_profile.realm,
    )

    activation_url = create_confirmation_link(obj, Confirmation.EMAIL_CHANGE)
    from zerver.context_processors import common_context

    context = common_context(user_profile)
    context.update(
        old_email=old_email,
        new_email=new_email,
        activate_url=activation_url,
    )
    language = user_profile.default_language
    send_email(
        "zerver/emails/confirm_new_email",
        to_emails=[new_email],
        from_name=FromAddress.security_email_from_name(language=language),
        from_address=FromAddress.tokenized_no_reply_address(),
        language=language,
        context=context,
        realm=user_profile.realm,
    )
Пример #4
0
 def test_build_SES_compatible_From_field(self) -> None:
     hamlet = self.example_user("hamlet")
     from_name = FromAddress.security_email_from_name(language="en")
     mail = build_email(
         "zerver/emails/password_reset",
         to_emails=[hamlet],
         from_name=from_name,
         from_address=FromAddress.NOREPLY,
         language="en",
     )
     self.assertEqual(mail.extra_headers["From"],
                      "{} <{}>".format(from_name, FromAddress.NOREPLY))
Пример #5
0
def confirm_email_change(request: HttpRequest,
                         confirmation_key: str) -> HttpResponse:
    try:
        email_change_object = get_object_from_key(confirmation_key,
                                                  [Confirmation.EMAIL_CHANGE])
    except ConfirmationKeyException as exception:
        return render_confirmation_key_error(request, exception)

    new_email = email_change_object.new_email
    old_email = email_change_object.old_email
    user_profile = email_change_object.user_profile

    if user_profile.realm.deactivated:
        return redirect_to_deactivation_notice()

    if not user_profile.is_active:
        # TODO: Make this into a user-facing error, not JSON
        raise UserDeactivatedError()

    if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin:
        raise JsonableError(
            _("Email address changes are disabled in this organization."))

    do_change_user_delivery_email(user_profile, new_email)

    context = {"realm_name": user_profile.realm.name, "new_email": new_email}
    language = user_profile.default_language
    send_email(
        "zerver/emails/notify_change_in_email",
        to_emails=[old_email],
        from_name=FromAddress.security_email_from_name(
            user_profile=user_profile),
        from_address=FromAddress.SUPPORT,
        language=language,
        context=context,
        realm=user_profile.realm,
    )

    ctx = {
        "new_email_html_tag":
        SafeString(
            f'<a href="mailto:{escape(new_email)}">{escape(new_email)}</a>'),
        "old_email_html_tag":
        SafeString(
            f'<a href="mailto:{escape(old_email)}">{escape(old_email)}</a>'),
    }
    return render(request,
                  "confirmation/confirm_email_change.html",
                  context=ctx)
Пример #6
0
    def test_send_email_exceptions(self) -> None:
        hamlet = self.example_user("hamlet")
        from_name = FromAddress.security_email_from_name(language="en")
        address = FromAddress.NOREPLY
        # Used to check the output
        mail = build_email(
            "zerver/emails/password_reset",
            to_emails=[hamlet],
            from_name=from_name,
            from_address=address,
            language="en",
        )
        self.assertEqual(mail.extra_headers["From"],
                         f"{from_name} <{FromAddress.NOREPLY}>")

        # We test the cases that should raise an EmailNotDeliveredException
        errors = {
            f"Unknown error sending password_reset email to {mail.to}": [0],
            f"Error sending password_reset email to {mail.to}":
            [SMTPException()],
            f"Error sending password_reset email to {mail.to}: {{'{address}': (550, b'User unknown')}}":
            [
                SMTPRecipientsRefused(
                    recipients={address: (550, b"User unknown")})
            ],
            f"Error sending password_reset email to {mail.to} with error code 242: From field too long":
            [SMTPDataError(242, "From field too long.")],
        }

        for message, side_effect in errors.items():
            with mock.patch.object(EmailBackend,
                                   "send_messages",
                                   side_effect=side_effect):
                with self.assertLogs(logger=logger) as info_log:
                    with self.assertRaises(EmailNotDeliveredException):
                        send_email(
                            "zerver/emails/password_reset",
                            to_emails=[hamlet],
                            from_name=from_name,
                            from_address=FromAddress.NOREPLY,
                            language="en",
                        )
                self.assert_length(info_log.records, 2)
                self.assertEqual(
                    info_log.output[0],
                    f"INFO:{logger.name}:Sending password_reset email to {mail.to}",
                )
                self.assertTrue(info_log.output[1].startswith(
                    f"ERROR:zulip.send_email:{message}"))
Пример #7
0
def email_on_new_login(sender: Any, user: UserProfile, request: Any,
                       **kwargs: Any) -> None:
    if not user.enable_login_emails:
        return
    # We import here to minimize the dependencies of this module,
    # since it runs as part of `manage.py` initialization
    from zerver.context_processors import common_context

    if not settings.SEND_LOGIN_EMAILS:
        return

    if request:
        # If the user's account was just created, avoid sending an email.
        if (timezone_now() -
                user.date_joined).total_seconds() <= JUST_CREATED_THRESHOLD:
            return

        user_agent = request.META.get("HTTP_USER_AGENT", "").lower()

        context = common_context(user)
        context["user_email"] = user.delivery_email
        user_tz = user.timezone
        if user_tz == "":
            user_tz = timezone_get_current_timezone_name()
        local_time = timezone_now().astimezone(pytz.timezone(user_tz))
        if user.twenty_four_hour_time:
            hhmm_string = local_time.strftime("%H:%M")
        else:
            hhmm_string = local_time.strftime("%I:%M%p")
        context["login_time"] = local_time.strftime(
            f"%A, %B %d, %Y at {hhmm_string} %Z")
        context["device_ip"] = request.META.get("REMOTE_ADDR") or _(
            "Unknown IP address")
        context["device_os"] = get_device_os(user_agent) or _(
            "an unknown operating system")
        context["device_browser"] = get_device_browser(user_agent) or _(
            "An unknown browser")
        context["unsubscribe_link"] = one_click_unsubscribe_link(user, "login")

        email_dict = {
            "template_prefix": "zerver/emails/notify_new_login",
            "to_user_ids": [user.id],
            "from_name":
            FromAddress.security_email_from_name(user_profile=user),
            "from_address": FromAddress.NOREPLY,
            "context": context,
        }
        queue_json_publish("email_senders", email_dict)
Пример #8
0
def email_on_new_login(sender: Any, user: UserProfile, request: Any,
                       **kwargs: Any) -> None:
    if not user.enable_login_emails:
        return
    # We import here to minimize the dependencies of this module,
    # since it runs as part of `manage.py` initialization
    from zerver.context_processors import common_context

    if not settings.SEND_LOGIN_EMAILS:
        return

    if request:
        # If the user's account was just created, avoid sending an email.
        if (timezone_now() -
                user.date_joined).total_seconds() <= JUST_CREATED_THRESHOLD:
            return

        user_agent = request.META.get('HTTP_USER_AGENT', "").lower()

        context = common_context(user)
        context['user_email'] = user.delivery_email
        user_tz = user.timezone
        if user_tz == '':
            user_tz = timezone_get_current_timezone_name()
        local_time = timezone_now().astimezone(get_timezone(user_tz))
        if user.twenty_four_hour_time:
            hhmm_string = local_time.strftime('%H:%M')
        else:
            hhmm_string = local_time.strftime('%I:%M%p')
        context['login_time'] = local_time.strftime(
            '%A, %B %d, %Y at {} %Z'.format(hhmm_string))
        context['device_ip'] = request.META.get('REMOTE_ADDR') or _(
            "Unknown IP address")
        context['device_os'] = get_device_os(user_agent) or _(
            "an unknown operating system")
        context['device_browser'] = get_device_browser(user_agent) or _(
            "An unknown browser")
        context['unsubscribe_link'] = one_click_unsubscribe_link(user, 'login')

        email_dict = {
            'template_prefix': 'zerver/emails/notify_new_login',
            'to_user_ids': [user.id],
            'from_name':
            FromAddress.security_email_from_name(user_profile=user),
            'from_address': FromAddress.NOREPLY,
            'context': context
        }
        queue_json_publish("email_senders", email_dict)
Пример #9
0
    def send(self, users: List[UserProfile]) -> None:
        """Sends one-use only links for resetting password to target users

        """
        for user_profile in users:
            context = {
                'email': user_profile.delivery_email,
                'reset_url': generate_password_reset_url(user_profile, default_token_generator),
                'realm_uri': user_profile.realm.uri,
                'realm_name': user_profile.realm.name,
                'active_account_in_realm': True,
            }
            send_email('zerver/emails/password_reset', to_user_ids=[user_profile.id],
                       from_address=FromAddress.tokenized_no_reply_address(),
                       from_name=FromAddress.security_email_from_name(user_profile=user_profile),
                       context=context)
Пример #10
0
    def test_send_email_exceptions(self) -> None:
        hamlet = self.example_user("hamlet")
        from_name = FromAddress.security_email_from_name(language="en")
        # Used to check the output
        mail = build_email(
            "zerver/emails/password_reset",
            to_emails=[hamlet],
            from_name=from_name,
            from_address=FromAddress.NOREPLY,
            language="en",
        )
        self.assertEqual(mail.extra_headers["From"],
                         "{} <{}>".format(from_name, FromAddress.NOREPLY))

        # We test the two cases that should raise an EmailNotDeliveredException
        errors = {
            f"Unknown error sending password_reset email to {mail.to}": [0],
            f"Error sending password_reset email to {mail.to}":
            [SMTPException()],
        }

        for message, side_effect in errors.items():
            with mock.patch.object(EmailBackend,
                                   "send_messages",
                                   side_effect=side_effect):
                with self.assertLogs(logger=logger) as info_log:
                    with self.assertRaises(EmailNotDeliveredException):
                        send_email(
                            "zerver/emails/password_reset",
                            to_emails=[hamlet],
                            from_name=from_name,
                            from_address=FromAddress.NOREPLY,
                            language="en",
                        )
                self.assert_length(info_log.records, 2)
                self.assertEqual(
                    info_log.output[0],
                    f"INFO:{logger.name}:Sending password_reset email to {mail.to}",
                )
                self.assertTrue(info_log.output[1].startswith(
                    f"ERROR:zulip.send_email:{message}"))
Пример #11
0
def downgrade_small_realms_behind_on_payments_as_needed() -> None:
    customers = Customer.objects.all()
    for customer in customers:
        realm = customer.realm

        # For larger realms, we generally want to talk to the customer
        # before downgrading; so this logic only applies with 5.
        if get_latest_seat_count(realm) >= 5:
            continue

        if get_current_plan_by_customer(customer) is None:
            continue

        due_invoice_count = 0
        for invoice in stripe.Invoice.list(
                customer=customer.stripe_customer_id, limit=2):
            if invoice.status == "open":
                due_invoice_count += 1

        # Customers with only 1 overdue invoice are ignored.
        if due_invoice_count < 2:
            continue

        # We've now decided to downgrade this customer and void all invoices, and the below will execute this.

        downgrade_now_without_creating_additional_invoices(realm)
        void_all_open_invoices(realm)
        context: Dict[str, str] = {
            "upgrade_url": f"{realm.uri}{reverse('initial_upgrade')}",
            "realm": realm,
        }
        send_email_to_billing_admins_and_realm_owners(
            "zerver/emails/realm_auto_downgraded",
            realm,
            from_name=FromAddress.security_email_from_name(
                language=realm.default_language),
            from_address=FromAddress.tokenized_no_reply_address(),
            language=realm.default_language,
            context=context,
        )
Пример #12
0
    def test_send_email_exceptions(self) -> None:
        hamlet = self.example_user("hamlet")
        from_name = FromAddress.security_email_from_name(language="en")
        # Used to check the output
        mail = build_email(
            "zerver/emails/password_reset",
            to_emails=[hamlet],
            from_name=from_name,
            from_address=FromAddress.NOREPLY,
            language="en",
        )
        self.assertEqual(mail.extra_headers["From"],
                         "{} <{}>".format(from_name, FromAddress.NOREPLY))

        # We test the two cases that should raise an EmailNotDeliveredException
        side_effect = [0, SMTPException]

        with mock.patch.object(EmailBackend,
                               "send_messages",
                               side_effect=side_effect):
            for i in range(len(side_effect)):
                with self.assertLogs(logger=logger) as info_log:
                    with self.assertRaises(EmailNotDeliveredException):
                        send_email(
                            "zerver/emails/password_reset",
                            to_emails=[hamlet],
                            from_name=from_name,
                            from_address=FromAddress.NOREPLY,
                            language="en",
                        )
                self.assertEqual(len(info_log.records), 2)
                self.assertEqual(
                    info_log.output,
                    [
                        f"INFO:{logger.name}:Sending password_reset email to {mail.to}",
                        f"ERROR:{logger.name}:Error sending password_reset email to {mail.to}",
                    ],
                )
Пример #13
0
def do_send_realm_reactivation_email(
        realm: Realm, *, acting_user: Optional[UserProfile]) -> None:
    url = create_confirmation_link(realm, Confirmation.REALM_REACTIVATION)
    RealmAuditLog.objects.create(
        realm=realm,
        acting_user=acting_user,
        event_type=RealmAuditLog.REALM_REACTIVATION_EMAIL_SENT,
        event_time=timezone_now(),
    )
    context = {
        "confirmation_url": url,
        "realm_uri": realm.uri,
        "realm_name": realm.name
    }
    language = realm.default_language
    send_email_to_admins(
        "zerver/emails/realm_reactivation",
        realm,
        from_address=FromAddress.tokenized_no_reply_address(),
        from_name=FromAddress.security_email_from_name(language=language),
        language=language,
        context=context,
    )
Пример #14
0
    def save(self,
             domain_override: Optional[bool]=None,
             subject_template_name: str='registration/password_reset_subject.txt',
             email_template_name: str='registration/password_reset_email.html',
             use_https: bool=False,
             token_generator: PasswordResetTokenGenerator=default_token_generator,
             from_email: Optional[str]=None,
             request: HttpRequest=None,
             html_email_template_name: Optional[str]=None,
             extra_email_context: Optional[Dict[str, Any]]=None
             ) -> None:
        """
        If the email address has an account in the target realm,
        generates a one-use only link for resetting password and sends
        to the user.

        We send a different email if an associated account does not exist in the
        database, or an account does exist, but not in the realm.

        Note: We ignore protocol and the various email template arguments (those
        are an artifact of using Django's password reset framework).
        """
        email = self.cleaned_data["email"]

        realm = get_realm(get_subdomain(request))

        if not email_auth_enabled(realm):
            logging.info("Password reset attempted for %s even though password auth is disabled." % (email,))
            return
        if email_belongs_to_ldap(realm, email):
            # TODO: Ideally, we'd provide a user-facing error here
            # about the fact that they aren't allowed to have a
            # password in the Zulip server and should change it in LDAP.
            logging.info("Password reset not allowed for user in LDAP domain")
            return
        if realm.deactivated:
            logging.info("Realm is deactivated")
            return

        if settings.RATE_LIMITING:
            try:
                rate_limit_password_reset_form_by_email(email)
            except RateLimited:
                # TODO: Show an informative, user-facing error message.
                logging.info("Too many password reset attempts for email %s" % (email,))
                return

        user = None  # type: Optional[UserProfile]
        try:
            user = get_user_by_delivery_email(email, realm)
        except UserProfile.DoesNotExist:
            pass

        context = {
            'email': email,
            'realm_uri': realm.uri,
            'realm_name': realm.name,
        }

        if user is not None and not user.is_active:
            context['user_deactivated'] = True
            user = None

        if user is not None:
            context['active_account_in_realm'] = True
            context['reset_url'] = generate_password_reset_url(user, token_generator)
            send_email('zerver/emails/password_reset', to_user_ids=[user.id],
                       from_name=FromAddress.security_email_from_name(user_profile=user),
                       from_address=FromAddress.tokenized_no_reply_address(),
                       context=context)
        else:
            context['active_account_in_realm'] = False
            active_accounts_in_other_realms = UserProfile.objects.filter(
                delivery_email__iexact=email, is_active=True)
            if active_accounts_in_other_realms:
                context['active_accounts_in_other_realms'] = active_accounts_in_other_realms
            language = request.LANGUAGE_CODE
            send_email('zerver/emails/password_reset', to_emails=[email],
                       from_name=FromAddress.security_email_from_name(language=language),
                       from_address=FromAddress.tokenized_no_reply_address(),
                       language=language, context=context)
Пример #15
0
    def save(
        self,
        domain_override: Optional[str] = None,
        subject_template_name: str = "registration/password_reset_subject.txt",
        email_template_name: str = "registration/password_reset_email.html",
        use_https: bool = False,
        token_generator: PasswordResetTokenGenerator = default_token_generator,
        from_email: Optional[str] = None,
        request: Optional[HttpRequest] = None,
        html_email_template_name: Optional[str] = None,
        extra_email_context: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        If the email address has an account in the target realm,
        generates a one-use only link for resetting password and sends
        to the user.

        We send a different email if an associated account does not exist in the
        database, or an account does exist, but not in the realm.

        Note: We ignore protocol and the various email template arguments (those
        are an artifact of using Django's password reset framework).
        """
        email = self.cleaned_data["email"]
        # The form is only used in zerver.views.auth.password_rest, we know that
        # the request must not be None
        assert request is not None

        realm = get_realm(get_subdomain(request))

        if not email_auth_enabled(realm):
            logging.info(
                "Password reset attempted for %s even though password auth is disabled.",
                email)
            return
        if email_belongs_to_ldap(realm, email):
            # TODO: Ideally, we'd provide a user-facing error here
            # about the fact that they aren't allowed to have a
            # password in the Zulip server and should change it in LDAP.
            logging.info("Password reset not allowed for user in LDAP domain")
            return
        if realm.deactivated:
            logging.info("Realm is deactivated")
            return

        if settings.RATE_LIMITING:
            try:
                rate_limit_password_reset_form_by_email(email)
                rate_limit_request_by_ip(request, domain="sends_email_by_ip")
            except RateLimited:
                logging.info(
                    "Too many password reset attempts for email %s from %s",
                    email,
                    request.META["REMOTE_ADDR"],
                )
                # The view will handle the RateLimit exception and render an appropriate page
                raise

        user: Optional[UserProfile] = None
        try:
            user = get_user_by_delivery_email(email, realm)
        except UserProfile.DoesNotExist:
            pass

        context = {
            "email": email,
            "realm_uri": realm.uri,
            "realm_name": realm.name,
        }

        if user is not None and not user.is_active:
            context["user_deactivated"] = True
            user = None

        if user is not None:
            context["active_account_in_realm"] = True
            context["reset_url"] = generate_password_reset_url(
                user, token_generator)
            queue_soft_reactivation(user.id)
            send_email(
                "zerver/emails/password_reset",
                to_user_ids=[user.id],
                from_name=FromAddress.security_email_from_name(
                    user_profile=user),
                from_address=FromAddress.tokenized_no_reply_address(),
                context=context,
                realm=realm,
                request=request,
            )
        else:
            context["active_account_in_realm"] = False
            active_accounts_in_other_realms = UserProfile.objects.filter(
                delivery_email__iexact=email, is_active=True)
            if active_accounts_in_other_realms:
                context[
                    "active_accounts_in_other_realms"] = active_accounts_in_other_realms
            language = get_language()

            send_email(
                "zerver/emails/password_reset",
                to_emails=[email],
                from_name=FromAddress.security_email_from_name(
                    language=language),
                from_address=FromAddress.tokenized_no_reply_address(),
                language=language,
                context=context,
                realm=realm,
                request=request,
            )