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)
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)
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, )
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))
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)
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}"))
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)
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)
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)
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}"))
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, )
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}", ], )
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, )
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)
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, )