diff --git a/src/pretix/base/models/customers.py b/src/pretix/base/models/customers.py index 942ae5918d..1e6ffafbf2 100644 --- a/src/pretix/base/models/customers.py +++ b/src/pretix/base/models/customers.py @@ -40,6 +40,7 @@ from i18nfield.fields import I18nCharField from phonenumber_field.modelfields import PhoneNumberField from pretix.base.banlist import banned +from pretix.base.i18n import language from pretix.base.models.base import LoggedModel from pretix.base.models.fields import MultiStringField from pretix.base.models.giftcards import GiftCardTransaction @@ -164,6 +165,28 @@ class Customer(LoggedModel): self.attendee_profiles.all().delete() self.invoice_addresses.all().delete() + def send_security_notice(self, message, email=None): + from pretix.base.services.mail import SendMailException, mail + from pretix.multidomain.urlreverse import build_absolute_uri + + try: + with language(self.locale): + mail( + email or self.email, + self.organizer.settings.mail_subject_customer_security_notice, + self.organizer.settings.mail_text_customer_security_notice, + { + **self.get_email_context(), + 'message': str(message), + 'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index') + }, + customer=self, + organizer=self.organizer, + locale=self.locale + ) + except SendMailException: + pass # Already logged + @scopes_disabled() def assign_identifier(self): charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index e6d123d060..75e87ecf35 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -2925,6 +2925,28 @@ If you did not request a new password, please ignore this email. Best regards, +Your {organizer} team""")) # noqa: W291 + }, + 'mail_subject_customer_security_notice': { + 'type': LazyI18nString, + 'default': LazyI18nString.from_gettext(gettext_noop("Changes to your account at {organizer}")), + }, + 'mail_text_customer_security_notice': { + 'type': LazyI18nString, + 'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name}, + +the following change has been made to your account at {organizer}: + +{message} + +You can review and change your account settings here: + +{url} + +If this change was not performed by you, please contact us immediately. + +Best regards, + Your {organizer} team""")) # noqa: W291 }, 'smtp_use_custom': { diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index c8f63914f6..54b900e34e 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -633,6 +633,16 @@ class MailSettingsForm(SettingsForm): required=False, widget=I18nMarkdownTextarea, ) + mail_subject_customer_security_notice = I18nFormField( + label=_("Subject"), + required=False, + widget=I18nTextInput, + ) + mail_text_customer_security_notice = I18nFormField( + label=_("Text"), + required=False, + widget=I18nMarkdownTextarea, + ) base_context = { 'mail_text_customer_registration': ['customer', 'url'], @@ -641,6 +651,8 @@ class MailSettingsForm(SettingsForm): 'mail_subject_customer_email_change': ['customer', 'url'], 'mail_text_customer_reset': ['customer', 'url'], 'mail_subject_customer_reset': ['customer', 'url'], + 'mail_text_customer_security_notice': ['customer', 'url', 'message'], + 'mail_subject_customer_security_notice': ['customer', 'url', 'message'], } def _get_sample_context(self, base_parameters): @@ -654,6 +666,9 @@ class MailSettingsForm(SettingsForm): 'presale:organizer.customer.activate' ) + '?token=' + get_random_string(30) + if 'message' in base_parameters: + placeholders['message'] = _('Your password has been changed.') + if 'customer' in base_parameters: placeholders['name'] = pgettext_lazy('person_name_sample', 'John Doe') name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme] diff --git a/src/pretix/control/templates/pretixcontrol/organizers/mail.html b/src/pretix/control/templates/pretixcontrol/organizers/mail.html index a80483a0bc..463caeca16 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/mail.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/mail.html @@ -65,6 +65,9 @@ {% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %} {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_subject_customer_reset,mail_text_customer_reset" %} + + {% blocktrans asvar title_security_notice %}Customer account security notification{% endblocktrans %} + {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="security_notice" title=title_security_notice items="mail_subject_customer_security_notice,mail_text_customer_security_notice" %} diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index fa0dd884b7..cbdb66283c 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -286,6 +286,7 @@ class SetPasswordView(FormView): self.customer.is_verified = True self.customer.save() self.customer.log_action('pretix.customer.password.set', {}) + self.customer.send_security_notice(_("Your password has been changed.")) messages.success( self.request, _('Your new password has been set! You can now use it to log in.'), @@ -540,6 +541,7 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView): customer.set_password(form.cleaned_data['password']) customer.save() messages.success(self.request, _('Your changes have been saved.')) + customer.send_security_notice(_("Your password has been changed.")) update_customer_session_auth_hash(self.request, customer) return HttpResponseRedirect(self.get_success_url()) @@ -630,11 +632,15 @@ class ConfirmChangeView(View): try: with transaction.atomic(): + old_email = customer.email customer.email = data['email'] customer.save() customer.log_action('pretix.customer.changed', { 'email': data['email'] }) + msg = _('Your email address has been changed to {email}.').format(email=customer.email) + customer.send_security_notice(msg, email=old_email) + customer.send_security_notice(msg, email=customer.email) except IntegrityError: messages.success(request, _('Your email address has not been updated since the address is already in use ' 'for another customer account.')) diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py index 82d18f85ad..dee2eabb43 100644 --- a/src/tests/presale/test_customer.py +++ b/src/tests/presale/test_customer.py @@ -164,6 +164,7 @@ def test_org_resetpw(env, client): customer.refresh_from_db() assert customer.check_password('PANioMR62') assert customer.is_verified + assert len(djmail.outbox) == 2 @pytest.mark.django_db @@ -623,6 +624,7 @@ def test_change_email(env, client): customer.refresh_from_db() assert customer.email == 'john@example.org' assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == ['john@example.com'] token = dumps({ 'customer': customer.pk, @@ -632,6 +634,9 @@ def test_change_email(env, client): assert r.status_code == 302 customer.refresh_from_db() assert customer.email == 'john@example.com' + assert len(djmail.outbox) == 3 + assert djmail.outbox[1].to == ['john@example.org'] + assert djmail.outbox[2].to == ['john@example.com'] @pytest.mark.django_db @@ -673,6 +678,7 @@ def test_change_pw(env, client, client2): r = client.get('/bigevents/account/password') assert r.status_code == 200 + assert len(djmail.outbox) == 1 # Client 2 got logged out r = client2.post('/bigevents/account/password')