diff --git a/doc/admin/config.rst b/doc/admin/config.rst index d508fb952..04f2772be 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -220,12 +220,30 @@ Example:: ``user``, ``password`` The SMTP user data to use for the connection. Empty by default. +``tls``, ``ssl`` + Use STARTTLS or SSL for the SMTP connection. Off by default. + ``from`` The email address to set as ``From`` header in outgoing emails by the system. Default: ``pretix@localhost`` -``tls``, ``ssl`` - Use STARTTLS or SSL for the SMTP connection. Off by default. +``from_notifications`` + The email address to set as ``From`` header in admin notification emails by the system. + Defaults to the value of ``from``. + +``from_organizers`` + The email address to set as ``From`` header in outgoing emails by the system sent on behalf of organizers. + Defaults to the value of ``from``. + +``custom_sender_verification_required`` + If this is on (the default), organizers need to verify email addresses they want to use as senders in their event. + +``custom_sender_spf_string`` + If this is set to a valid SPF string, pretix will show a warning if organizers use a sender address from a domain + that does not include this value. + +``custom_smtp_allow_private_networks`` + If this is off (the default), custom SMTP servers cannot be private network addresses. ``admins`` Comma-separated list of email addresses that should receive a report about every error code 500 thrown by pretix. diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 855681c63..bfc0668fa 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -713,7 +713,6 @@ class EventSettingsSerializer(SettingsSerializer): 'ticket_download_require_validated_email', 'ticket_secret_length', 'mail_prefix', - 'mail_from', 'mail_from_name', 'mail_attach_ical', 'mail_attach_tickets', diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 13d43a137..c721dccda 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -665,13 +665,13 @@ class Event(EventMixin, LoggedModel): return locking.LockManager(self) - def get_mail_backend(self, timeout=None, force_custom=False): + def get_mail_backend(self, timeout=None): """ Returns an email server connection, either by using the system-wide connection or by returning a custom one based on the event's settings. """ - if self.settings.smtp_use_custom or force_custom: + if self.settings.smtp_use_custom: return get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND, host=self.settings.smtp_host, port=self.settings.smtp_port, diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 7a842c7da..96633995c 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -191,12 +191,12 @@ class Organizer(LoggedModel): e.delete() self.teams.all().delete() - def get_mail_backend(self, timeout=None, force_custom=False): + def get_mail_backend(self, timeout=None): """ Returns an email server connection, either by using the system-wide connection or by returning a custom one based on the organizer's settings. """ - if self.settings.smtp_use_custom or force_custom: + if self.settings.smtp_use_custom: return get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND, host=self.settings.smtp_host, port=self.settings.smtp_port, diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 6bc8a7729..ae87fdf7c 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -217,7 +217,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La for bcc_mail in settings_holder.settings.mail_bcc.split(','): bcc.append(bcc_mail.strip()) - if settings_holder.settings.mail_from == settings.DEFAULT_FROM_EMAIL and settings_holder.settings.contact_mail and not headers.get('Reply-To'): + if settings_holder.settings.mail_from not in (settings.DEFAULT_FROM_EMAIL, settings.MAIL_FROM_ORGANIZERS) \ + and settings_holder.settings.contact_mail and not headers.get('Reply-To'): headers['Reply-To'] = settings_holder.settings.contact_mail prefix = settings_holder.settings.get('mail_prefix') diff --git a/src/pretix/base/services/notifications.py b/src/pretix/base/services/notifications.py index 15dfd6664..b88a518a5 100644 --- a/src/pretix/base/services/notifications.py +++ b/src/pretix/base/services/notifications.py @@ -148,7 +148,7 @@ def send_notification_mail(notification: Notification, user: User): ), 'body': body_plain, 'html': body_html, - 'sender': settings.MAIL_FROM, + 'sender': settings.MAIL_FROM_NOTIFICATIONS, 'headers': {}, 'user': user.pk }) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 1281004e9..914d76b17 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1589,7 +1589,7 @@ DEFAULTS = { 'type': str }, 'mail_from': { - 'default': settings.MAIL_FROM, + 'default': settings.MAIL_FROM_ORGANIZERS, 'type': str, 'form_class': forms.EmailField, 'serializer_class': serializers.EmailField, diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py index d3345b606..1fb86d232 100644 --- a/src/pretix/control/forms/__init__.py +++ b/src/pretix/control/forms/__init__.py @@ -48,7 +48,7 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django_scopes.forms import SafeModelMultipleChoiceField -from ...base.forms import I18nModelForm, SecretKeySettingsField +from ...base.forms import I18nModelForm # Import for backwards compatibility with okd import paths from ...base.forms.widgets import ( # noqa @@ -373,49 +373,6 @@ class FontSelect(forms.RadioSelect): option_template_name = 'pretixcontrol/font_option.html' -class SMTPSettingsMixin(forms.Form): - smtp_use_custom = forms.BooleanField( - label=_("Use custom SMTP server"), - help_text=_("All mail related to your event will be sent over the smtp server specified by you."), - required=False - ) - smtp_host = forms.CharField( - label=_("Hostname"), - required=False, - widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'}) - ) - smtp_port = forms.IntegerField( - label=_("Port"), - required=False, - widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'}) - ) - smtp_username = forms.CharField( - label=_("Username"), - widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}), - required=False - ) - smtp_password = SecretKeySettingsField( - label=_("Password"), - required=False, - ) - smtp_use_tls = forms.BooleanField( - label=_("Use STARTTLS"), - help_text=_("Commonly enabled on port 587."), - required=False - ) - smtp_use_ssl = forms.BooleanField( - label=_("Use SSL"), - help_text=_("Commonly enabled on port 465."), - required=False - ) - - def clean(self): - data = super().clean() - if data.get('smtp_use_tls') and data.get('smtp_use_ssl'): - raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.')) - return data - - class ItemMultipleChoiceField(SafeModelMultipleChoiceField): def label_from_instance(self, obj): return str(obj) if obj.active else mark_safe(f'{escape(obj)}') diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index ea0580c3a..35745896a 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -64,7 +64,7 @@ from pretix.base.settings import ( PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings, ) from pretix.control.forms import ( - MultipleLanguagesWidget, SlugWidget, SMTPSettingsMixin, SplitDateTimeField, + MultipleLanguagesWidget, SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget, ) from pretix.control.forms.widgets import Select2 @@ -865,10 +865,9 @@ def contains_web_channel_validate(val): raise ValidationError(_("The online shop must be selected to receive these emails.")) -class MailSettingsForm(SMTPSettingsMixin, SettingsForm): +class MailSettingsForm(SettingsForm): auto_fields = [ 'mail_prefix', - 'mail_from', 'mail_from_name', 'mail_attach_ical', 'mail_attach_tickets', diff --git a/src/pretix/control/forms/mailsetup.py b/src/pretix/control/forms/mailsetup.py new file mode 100644 index 000000000..d3c0e91c1 --- /dev/null +++ b/src/pretix/control/forms/mailsetup.py @@ -0,0 +1,129 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-2021 rami.io GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +import ipaddress +import socket + +from django import forms +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from pretix.base.forms import SecretKeySettingsField, SettingsForm + + +class SMTPMailForm(SettingsForm): + mail_from = forms.EmailField( + label=_("Sender address"), + help_text=_("Sender address for outgoing emails"), + required=True, + ) + smtp_host = forms.CharField( + label=_("Hostname"), + required=True, + widget=forms.TextInput(attrs={'placeholder': 'mail.example.org'}) + ) + smtp_port = forms.IntegerField( + label=_("Port"), + required=True, + widget=forms.TextInput(attrs={'placeholder': 'e.g. 587, 465, 25, ...'}) + ) + smtp_username = forms.CharField( + label=_("Username"), + widget=forms.TextInput(attrs={'placeholder': 'myuser@example.org'}), + required=False + ) + smtp_password = SecretKeySettingsField( + label=_("Password"), + required=False, + ) + smtp_use_tls = forms.BooleanField( + label=_("Use STARTTLS"), + help_text=_("Commonly enabled on port 587."), + required=False + ) + smtp_use_ssl = forms.BooleanField( + label=_("Use SSL"), + help_text=_("Commonly enabled on port 465."), + required=False + ) + + def clean(self): + data = super().clean() + if data.get('smtp_use_tls') and data.get('smtp_use_ssl'): + raise ValidationError(_('You can activate either SSL or STARTTLS security, but not both at the same time.')) + for k, v in self.fields.items(): + val = data.get(k) + if v._required and not val: + self.add_error(k, _('This field is required.')) + return data + + def clean_smtp_host(self): + v = self.cleaned_data['smtp_host'] + if not settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS: + try: + if ipaddress.ip_address(v).is_private: + raise ValidationError(_('You are not allowed to use this mail server, please choose one with a ' + 'public IP address instead.')) + except ValueError: + try: + if ipaddress.ip_address(socket.gethostbyname(v)).is_private: + raise ValidationError(_('You are not allowed to use this mail server, please choose one with a ' + 'public IP address instead.')) + except OSError: + raise ValidationError(_('We were unable to resolve this hostname.')) + return v + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.obj.settings.mail_from in (settings.MAIL_FROM, settings.MAIL_FROM_ORGANIZERS): + self.initial.pop('mail_from') + + for k, v in self.fields.items(): + v._required = v.required + v.required = False + v.widget.is_required = False + + +class SimpleMailForm(SettingsForm): + mail_from = forms.EmailField( + label=_("Sender address"), + help_text=_("Sender address for outgoing emails"), + required=True, + ) + + def clean(self): + cleaned_data = super().clean() + for k, v in self.fields.items(): + val = cleaned_data.get(k) + if v._required and not val: + self.add_error(k, _('This field is required.')) + return cleaned_data + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.obj.settings.mail_from in (settings.MAIL_FROM, settings.MAIL_FROM_ORGANIZERS): + self.initial.pop('mail_from') + + for k, v in self.fields.items(): + v._required = v.required + v.required = False + v.widget.is_required = False diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 18cc8c476..7631c64bc 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -60,9 +60,7 @@ from pretix.base.models import ( MembershipType, Organizer, Team, ) from pretix.base.settings import PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS -from pretix.control.forms import ( - ExtFileField, SMTPSettingsMixin, SplitDateTimeField, -) +from pretix.control.forms import ExtFileField, SplitDateTimeField from pretix.control.forms.event import ( SafeEventMultipleChoiceField, multimail_validate, ) @@ -358,9 +356,8 @@ class OrganizerSettingsForm(SettingsForm): ] -class MailSettingsForm(SMTPSettingsMixin, SettingsForm): +class MailSettingsForm(SettingsForm): auto_fields = [ - 'mail_from', 'mail_from_name', ] diff --git a/src/pretix/control/templates/pretixcontrol/email/email_setup.txt b/src/pretix/control/templates/pretixcontrol/email/email_setup.txt new file mode 100644 index 000000000..d3780c3bb --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/email/email_setup.txt @@ -0,0 +1,14 @@ +{% load i18n %}{% blocktrans with code=code instance=instance %}Hello, + +someone requested to use {{ address }} as a sender address on {{ instance }}. +This will allow them to send emails that are shown to originate from this email address. +If that was you, please enter the following confirmation code: + +{{ code }} + +If this was not requested by you, you can safely ignore this email. + +Best regards, + +Your {{ instance }} team +{% endblocktrans %} \ No newline at end of file diff --git a/src/pretix/control/templates/pretixcontrol/email_setup.html b/src/pretix/control/templates/pretixcontrol/email_setup.html new file mode 100644 index 000000000..4971f1be1 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/email_setup.html @@ -0,0 +1,127 @@ +{% extends basetpl %} +{% load i18n %} +{% load bootstrap3 %} +{% load hierarkey_form %} +{% load static %} +{% block title %}{% trans "Organizer" %}{% endblock %} +{% block content %} +

{% trans "E-mail sending" %}

+
+ {% csrf_token %} +
+
+
+
+

+ + +

+
+
+ +
+
+
+
+

+ + +

+
+
+ +
+
+
+
+

+ + +

+
+
+ +
+ {% if request.event %} +
+
+
+

+ + +

+
+
+ +
+ {% endif %} +
+ +
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/email_setup_simple.html b/src/pretix/control/templates/pretixcontrol/email_setup_simple.html new file mode 100644 index 000000000..05ec59d8e --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/email_setup_simple.html @@ -0,0 +1,79 @@ +{% extends basetpl %} +{% load i18n %} +{% load bootstrap3 %} +{% load hierarkey_form %} +{% load static %} +{% block title %}{% trans "Organizer" %}{% endblock %} +{% block content %} +

{% trans "E-mail sending" %}

+
+ {% csrf_token %} + {% for k, v in request.POST.items %} + + {% endfor %} + +
+
+

+ {% trans "Use system email server with a custom sender address" %} +

+
+
+ {% if spf_warning %} +
+

+ {{ spf_warning }} +

+ {% if spf_record %} +

+ {% trans "This is the SPF record we found on your domain:" %} +

+
{{ spf_record }}
+

+ {% trans "To fix this, include the following part before the last word:" %} +

+
{{ spf_key }}
+ {% else %} +

+ {% trans "Your new SPF record could look like this:" %} +

+
v=spf1 a mx {{ spf_key }} ~all
+ {% endif %} +

+ {% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %} +

+
+ {% elif spf_key %} +
+ {% blocktrans trimmed %} + We found an SPF record on your domain that includes this system. Great! + {% endblocktrans %} +
+ {% endif %} + {% if verification %} +

{% trans "Verification" %}

+

+ {% blocktrans trimmed with recp=recp %} + We've sent an email to {{ recp }} with a confirmation code to verify that this email address + is owned by you. Please enter the verification code below: + {% endblocktrans %} +

+
+ +
+ +
+
+ {% endif %} +
+
+ +
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/email_setup_smtp.html b/src/pretix/control/templates/pretixcontrol/email_setup_smtp.html new file mode 100644 index 000000000..9709493b8 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/email_setup_smtp.html @@ -0,0 +1,42 @@ +{% extends basetpl %} +{% load i18n %} +{% load bootstrap3 %} +{% load hierarkey_form %} +{% load static %} +{% block title %}{% trans "Organizer" %}{% endblock %} +{% block content %} +

{% trans "E-mail sending" %}

+
+ {% csrf_token %} + {% for k, v in request.POST.items %} + + {% endfor %} + +
+
+

+ {% trans "Use a custom SMTP server" %} +

+
+
+
+ {% blocktrans trimmed %} + A test connection to your SMTP server was successful. You can now save your new settings + to put them in use. + {% endblocktrans %} +
+ {% if known_host_problem %} +
+ {{ known_host_problem }} +
+ {% endif %} +
+
+ +
+ +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/mail.html b/src/pretix/control/templates/pretixcontrol/event/mail.html index 06c41fe55..c7dd99a77 100644 --- a/src/pretix/control/templates/pretixcontrol/event/mail.html +++ b/src/pretix/control/templates/pretixcontrol/event/mail.html @@ -12,16 +12,49 @@
{% trans "General" %} + {% bootstrap_field form.mail_prefix layout="control" %} + {% bootstrap_field form.mail_attach_tickets layout="control" %} + {% bootstrap_field form.mail_attach_ical layout="control" %} {% url "control:organizer.settings.mail" organizer=request.organizer.slug as org_url %} - {% propagated request.event org_url "mail_from" "mail_from_name" "mail_text_signature" "mail_bcc" %} - {% bootstrap_field form.mail_from layout="control" %} + {% propagated request.event org_url "mail_from" "smtp_use_custom" "smtp_host" "smtp_port" "smtp_username" "smtp_password" "smtp_use_tls" "smtp_use_ssl" %} +
+ +
+ {% if request.event.settings.smtp_use_custom %} + {% trans "Custom SMTP server" %}: {{ request.event.settings.smtp_host }} + {% else %} + {% trans "System-provided email server" %} + {% endif %} +    + + + {% trans "Edit" %} + +
+
+
+ +
+ {{ request.event.settings.mail_from }} +    + + + {% trans "Edit" %} + +
+
+ {% endpropagated %} + {% propagated request.event org_url "mail_from_name" "mail_text_signature" "mail_bcc" %} {% bootstrap_field form.mail_from_name layout="control" %} {% bootstrap_field form.mail_text_signature layout="control" %} {% bootstrap_field form.mail_bcc layout="control" %} {% endpropagated %} - {% bootstrap_field form.mail_prefix layout="control" %} - {% bootstrap_field form.mail_attach_tickets layout="control" %} - {% bootstrap_field form.mail_attach_ical layout="control" %} {% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
@@ -85,26 +118,11 @@

{% trans "Attachments" %}

{% bootstrap_field form.mail_attachment_new_order layout="control" %}
-
- {% trans "SMTP settings" %} - {% propagated request.event org_url "smtp_use_custom" "smtp_host" "smtp_port" "smtp_username" "smtp_password" "smtp_use_tls" "smtp_use_ssl" %} - {% bootstrap_field form.smtp_use_custom layout="control" %} - {% bootstrap_field form.smtp_host layout="control" %} - {% bootstrap_field form.smtp_port layout="control" %} - {% bootstrap_field form.smtp_username layout="control" %} - {% bootstrap_field form.smtp_password layout="control" %} - {% bootstrap_field form.smtp_use_tls layout="control" %} - {% bootstrap_field form.smtp_use_ssl layout="control" %} - {% endpropagated %} -
-
{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/mail.html b/src/pretix/control/templates/pretixcontrol/organizers/mail.html index 3b1968a37..1119d46f1 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/mail.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/mail.html @@ -11,13 +11,45 @@

{% trans "E-mail settings" %}

+ mail-preview-url="{% url "control:organizer.settings.mail.preview" organizer=request.organizer.slug %}"> {% csrf_token %} {% bootstrap_form_errors form %}
{% trans "General" %} - {% bootstrap_field form.mail_from layout="control" %} +
+ +
+ {% if request.organizer.settings.smtp_use_custom %} + {% trans "Custom SMTP server" %}: {{ request.organizer.settings.smtp_host }} + {% else %} + {% trans "System-provided email server" %} + {% endif %} +    + + + {% trans "Edit" %} + +
+
+
+ +
+ {{ request.organizer.settings.mail_from }} +    + + + {% trans "Edit" %} + +
+
+ {% bootstrap_field form.mail_from_name layout="control" %} {% bootstrap_field form.mail_text_signature layout="control" %} {% bootstrap_field form.mail_bcc layout="control" %} @@ -35,24 +67,11 @@ {% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_text_customer_reset" %}
-
- {% trans "SMTP settings" %} - {% bootstrap_field form.smtp_use_custom layout="control" %} - {% bootstrap_field form.smtp_host layout="control" %} - {% bootstrap_field form.smtp_port layout="control" %} - {% bootstrap_field form.smtp_username layout="control" %} - {% bootstrap_field form.smtp_password layout="control" %} - {% bootstrap_field form.smtp_use_tls layout="control" %} - {% bootstrap_field form.smtp_use_ssl layout="control" %} -
-
{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 8042f121c..ed1ac7698 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -112,6 +112,8 @@ urlpatterns = [ re_path(r'^organizer/(?P[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'), re_path(r'^organizer/(?P[^/]+)/settings/email$', organizer.OrganizerMailSettings.as_view(), name='organizer.settings.mail'), + re_path(r'^organizer/(?P[^/]+)/settings/email/setup$', + organizer.MailSettingsSetup.as_view(), name='organizer.settings.mail.setup'), re_path(r'^organizer/(?P[^/]+)/settings/email/preview$', organizer.MailSettingsPreview.as_view(), name='organizer.settings.mail.preview'), re_path(r'^organizer/(?P[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'), @@ -214,6 +216,7 @@ urlpatterns = [ re_path(r'^settings/tickets/preview/(?P[^/]+)$', event.TicketSettingsPreview.as_view(), name='event.settings.tickets.preview'), re_path(r'^settings/email$', event.MailSettings.as_view(), name='event.settings.mail'), + re_path(r'^settings/email/setup$', event.MailSettingsSetup.as_view(), name='event.settings.mail.setup'), re_path(r'^settings/email/preview$', event.MailSettingsPreview.as_view(), name='event.settings.mail.preview'), re_path(r'^settings/email/layoutpreview$', event.MailSettingsRendererPreview.as_view(), name='event.settings.mail.preview.layout'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 6eecc9d5b..51fee18af 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -65,9 +65,7 @@ from i18nfield.utils import I18nJSONEncoder from pytz import timezone from pretix.base.channels import get_all_sales_channels -from pretix.base.email import ( - get_available_placeholders, test_custom_smtp_backend, -) +from pretix.base.email import get_available_placeholders from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher from pretix.base.models.event import EventMetaValue from pretix.base.services import tickets @@ -83,6 +81,7 @@ from pretix.control.forms.event import ( TicketSettingsForm, WidgetCodeForm, ) from pretix.control.permissions import EventPermissionRequiredMixin +from pretix.control.views.mailsetup import MailSettingsSetupView from pretix.control.views.user import RecentAuthenticationRequiredMixin from pretix.helpers.database import rolledback_transaction from pretix.multidomain.urlreverse import get_event_domain @@ -639,29 +638,29 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView): k: form.cleaned_data.get(k) for k in form.changed_data } ) - - if request.POST.get('test', '0').strip() == '1': - backend = self.request.event.get_mail_backend(force_custom=True, timeout=10) - try: - test_custom_smtp_backend(backend, self.request.event.settings.mail_from) - except Exception as e: - messages.warning(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e)) - else: - if form.cleaned_data.get('smtp_use_custom'): - messages.success(self.request, _('Your changes have been saved and the connection attempt to ' - 'your SMTP server was successful.')) - else: - messages.success(self.request, _('We\'ve been able to contact the SMTP server you configured. ' - 'Remember to check the "use custom SMTP server" checkbox, ' - 'otherwise your SMTP server will not be used.')) - else: - messages.success(self.request, _('Your changes have been saved.')) + messages.success(self.request, _('Your changes have been saved.')) return redirect(self.get_success_url()) else: messages.error(self.request, _('We could not save your changes. See below for details.')) return self.get(request) +class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView): + permission = 'can_change_event_settings' + basetpl = 'pretixcontrol/event/base.html' + + def get_success_url(self) -> str: + return reverse('control:event.settings.mail', kwargs={ + 'organizer': self.request.event.organizer.slug, + 'event': self.request.event.slug + }) + + def log_action(self, data): + self.request.event.log_action( + 'pretix.event.settings', user=self.request.user, data=data + ) + + class MailSettingsPreview(EventPermissionRequiredMixin, View): permission = 'can_change_event_settings' diff --git a/src/pretix/control/views/mailsetup.py b/src/pretix/control/views/mailsetup.py new file mode 100644 index 000000000..d436294fc --- /dev/null +++ b/src/pretix/control/views/mailsetup.py @@ -0,0 +1,278 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-2021 rami.io GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +import logging + +import dns.resolver +from django.conf import settings +from django.contrib import messages +from django.core.mail import get_connection +from django.shortcuts import redirect +from django.utils.crypto import get_random_string +from django.utils.functional import cached_property +from django.utils.translation import gettext_lazy as _ +from django.views.generic import TemplateView + +from pretix.base import email +from pretix.base.models import Event +from pretix.base.services.mail import mail +from pretix.control.forms.filter import OrganizerFilterForm +from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm + +logger = logging.getLogger(__name__) + + +def get_spf_record(hostname): + try: + for resp in dns.resolver.resolve(hostname, 'TXT'): + data = b''.join(resp.strings).decode() + if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5 + return data + except: + logger.exception("Could not fetch SPF record") + + +def _check_spf_record(not_found_lookup_parts, spf_record, depth): + if depth > 10: # prevent infinite loops + return + + parts = spf_record.lower().split(" ") # RFC 7208, section 4.6.1 + + for p in parts: + try: + not_found_lookup_parts.remove(p) + except KeyError: + pass + + if not not_found_lookup_parts: # save some DNS requests if we already found everything + return + + for p in parts: + if p.startswith('include:') or p.startswith('+include:'): + _, hostname = p.split(':') + rec_record = get_spf_record(hostname) + if rec_record: + _check_spf_record(not_found_lookup_parts, rec_record, depth + 1) + + +def check_spf_record(lookup, spf_record): + """ + Check that all parts of lookup appear somewhere in the given SPF record, resolving + include: directives recursively + """ + not_found_lookup_parts = set(lookup.split(" ")) + _check_spf_record(not_found_lookup_parts, spf_record, 0) + return not not_found_lookup_parts + + +class MailSettingsSetupView(TemplateView): + template_name = 'pretixcontrol/email_setup.html' + basetpl = None + + @cached_property + def object(self): + return getattr(self.request, 'event', self.request.organizer) + + @cached_property + def smtp_form(self): + return SMTPMailForm( + obj=self.object, + prefix='smtp', + data=self.request.POST if (self.request.method == "POST" and self.request.POST.get("mode") == "smtp") else None, + ) + + @cached_property + def simple_form(self): + return SimpleMailForm( + obj=self.object, + prefix='simple', + data=self.request.POST if (self.request.method == "POST" and self.request.POST.get("mode") == "simple") else None, + ) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx['basetpl'] = self.basetpl + ctx['object'] = self.object + ctx['smtp_form'] = self.smtp_form + ctx['simple_form'] = self.simple_form + ctx['default_sender_address'] = settings.MAIL_FROM_ORGANIZERS + if 'mode' in self.request.POST: + ctx['mode'] = self.request.POST.get('mode') + elif self.object.settings.smtp_use_custom: + ctx['mode'] = 'smtp' + elif self.object.settings.mail_from not in (settings.MAIL_FROM_ORGANIZERS, settings.MAIL_FROM): + ctx['mode'] = 'simple' + else: + ctx['mode'] = 'system' + return ctx + + @cached_property + def filter_form(self): + return OrganizerFilterForm(data=self.request.GET, request=self.request) + + def post(self, request, *args, **kwargs): + if request.POST.get('mode') == 'system': + if isinstance(self.object, Event) and 'mail_from' in self.object.organizer.settings._cache(): + self.object.settings.mail_from = settings.MAIL_FROM_ORGANIZERS + else: + del self.object.settings.mail_from + self.object.settings.smtp_use_custom = False + del self.object.settings.smtp_host + del self.object.settings.smtp_port + del self.object.settings.smtp_username + del self.object.settings.smtp_password + del self.object.settings.smtp_use_tls + del self.object.settings.smtp_use_ssl + messages.success(request, _('Your changes have been saved.')) + return redirect(self.get_success_url()) + + elif request.POST.get('mode') == 'reset': + del self.object.settings.mail_from + del self.object.settings.smtp_use_custom + del self.object.settings.smtp_host + del self.object.settings.smtp_port + del self.object.settings.smtp_username + del self.object.settings.smtp_password + del self.object.settings.smtp_use_tls + del self.object.settings.smtp_use_ssl + messages.success(request, _('Your changes have been saved.')) + return redirect(self.get_success_url()) + + elif request.POST.get('mode') == 'simple': + if not self.simple_form.is_valid(): + return super().get(request, *args, **kwargs) + + session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}' + allow_save = ( + (not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or + ('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and + (not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save') + ) + + if allow_save: + for k, v in self.simple_form.cleaned_data.items(): + self.object.settings.set(k, v) + self.log_action(self.simple_form.cleaned_data) + if session_key in request.session: + del request.session[session_key] + messages.success(request, _('Your changes have been saved.')) + return redirect(self.get_success_url()) + + spf_warning = None + spf_record = None + if settings.MAIL_CUSTOM_SENDER_SPF_STRING: + hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1] + spf_record = get_spf_record(hostname) + if not spf_record: + spf_warning = _( + 'We could not find an SPF record set for the domain you are trying to use. You can still ' + 'proceed, but it will increase the chance of emails going to spam or being rejected. We ' + 'strongly recommend setting an SPF record on the domain. You can do so through the DNS ' + 'settings at the provider you registered your domain with.' + ) + elif not check_spf_record(settings.MAIL_CUSTOM_SENDER_SPF_STRING, spf_record): + spf_warning = _( + 'We found an SPF record set for the domain you are trying to use, but it does not include this ' + 'system\'s email server. This means that there is a very high chance most of the emails will be ' + 'rejected or marked as spam. You should update the DNS settings of your domain to include ' + 'this system in the SPF record.' + ) + + if settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED: + if 'verification' in self.request.POST: + messages.error(request, _('The verification code was incorrect, please try again.')) + else: + self.request.session[session_key] = get_random_string(length=6, allowed_chars='1234567890') + mail( + self.simple_form.cleaned_data.get('mail_from'), + _('Sender address verification'), + 'pretixcontrol/email/email_setup.txt', + { + 'code': self.request.session[session_key], + 'address': self.simple_form.cleaned_data.get('mail_from'), + 'instance': settings.PRETIX_INSTANCE_NAME, + }, + None, + locale=self.request.LANGUAGE_CODE, + user=self.request.user + ) + + return self.response_class( + request=self.request, + template='pretixcontrol/email_setup_simple.html', + context={ + 'basetpl': self.basetpl, + 'object': self.object, + 'verification': settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED, + 'spf_warning': spf_warning, + 'spf_record': spf_record, + 'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING, + 'recp': self.simple_form.cleaned_data.get('mail_from') + }, + using=self.template_engine, + ) + + elif request.POST.get('mode') == 'smtp': + if not self.smtp_form.is_valid(): + return super().get(request, *args, **kwargs) + + if request.POST.get('state') == 'save': + for k, v in self.smtp_form.cleaned_data.items(): + self.object.settings.set(k, v) + self.object.settings.smtp_use_custom = True + self.log_action({**self.smtp_form.cleaned_data, 'smtp_use_custom': True}) + messages.success(request, _('Your changes have been saved.')) + return redirect(self.get_success_url()) + else: + backend = get_connection( + backend=settings.EMAIL_CUSTOM_SMTP_BACKEND, + host=self.smtp_form.cleaned_data['smtp_host'], + port=self.smtp_form.cleaned_data['smtp_port'], + username=self.smtp_form.cleaned_data.get('smtp_username', ''), + password=self.smtp_form.cleaned_data.get('smtp_password', ''), + use_tls=self.smtp_form.cleaned_data.get('smtp_use_tls', False), + use_ssl=self.smtp_form.cleaned_data.get('smtp_use_ssl', False), + fail_silently=False, + timeout=10, + ) + try: + email.test_custom_smtp_backend(backend, self.smtp_form.cleaned_data.get('mail_from')) + except Exception as e: + messages.error(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e)) + return self.get(request, *args, **kwargs) + + return self.response_class( + request=self.request, + template='pretixcontrol/email_setup_smtp.html', + context={ + 'basetpl': self.basetpl, + 'object': self.object, + 'known_host_problem': { + 'smtp.gmail.com': _( + 'We recommend not using Google Mail for transactional emails. If you try sending many ' + 'emails in a short amount of time, e.g. when sending information to all your ticket ' + 'buyers, there is a high chance Google will not deliver all of your emails since they ' + 'impose a maximum number of emails per time period.' + ), + }.get(self.smtp_form.cleaned_data['smtp_host']), + }, + using=self.template_engine, + ) diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index 32a7ea659..60c01adb2 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -64,7 +64,6 @@ from django.views.generic import ( from pretix.api.models import WebHook from pretix.base.auth import get_auth_backends from pretix.base.channels import get_all_sales_channels -from pretix.base.email import test_custom_smtp_backend from pretix.base.i18n import language from pretix.base.models import ( CachedFile, Customer, Device, Gate, GiftCard, Invoice, LogEntry, @@ -102,6 +101,7 @@ from pretix.control.permissions import ( ) from pretix.control.signals import nav_organizer from pretix.control.views import PaginationMixin +from pretix.control.views.mailsetup import MailSettingsSetupView from pretix.helpers.dicts import merge_dicts from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.multidomain.urlreverse import build_absolute_uri @@ -262,22 +262,6 @@ class OrganizerMailSettings(OrganizerSettingsFormView): k: form.cleaned_data.get(k) for k in form.changed_data } ) - - if request.POST.get('test', '0').strip() == '1': - backend = self.request.organizer.get_mail_backend(force_custom=True, timeout=10) - try: - test_custom_smtp_backend(backend, self.request.organizer.settings.mail_from) - except Exception as e: - messages.warning(self.request, _('An error occurred while contacting the SMTP server: %s') % str(e)) - else: - if form.cleaned_data.get('smtp_use_custom'): - messages.success(self.request, _('Your changes have been saved and the connection attempt to ' - 'your SMTP server was successful.')) - else: - messages.success(self.request, _('We\'ve been able to contact the SMTP server you configured. ' - 'Remember to check the "use custom SMTP server" checkbox, ' - 'otherwise your SMTP server will not be used.')) - else: messages.success(self.request, _('Your changes have been saved.')) return redirect(self.get_success_url()) else: @@ -285,6 +269,21 @@ class OrganizerMailSettings(OrganizerSettingsFormView): return self.get(request) +class MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView): + permission = 'can_change_organizer_settings' + basetpl = 'pretixcontrol/base.html' + + def get_success_url(self): + return reverse('control:organizer.settings.mail', kwargs={ + 'organizer': self.request.organizer.slug, + }) + + def log_action(self, data): + self.request.organizer.log_action( + 'pretix.organizer.settings', user=self.request.user, data=data + ) + + class MailSettingsPreview(OrganizerPermissionRequiredMixin, View): permission = 'can_change_organizer_settings' diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 6a0782119..b52047db4 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -213,8 +213,12 @@ ALLOWED_HOSTS = ['*'] LANGUAGE_CODE = config.get('locale', 'default', fallback='en') TIME_ZONE = config.get('locale', 'timezone', fallback='UTC') -MAIL_FROM = SERVER_EMAIL = DEFAULT_FROM_EMAIL = config.get( - 'mail', 'from', fallback='pretix@localhost') +MAIL_FROM = SERVER_EMAIL = DEFAULT_FROM_EMAIL = config.get('mail', 'from', fallback='pretix@localhost') +MAIL_FROM_NOTIFICATIONS = config.get('mail', 'from_notifications', fallback=MAIL_FROM) +MAIL_FROM_ORGANIZERS = config.get('mail', 'from_organizers', fallback=MAIL_FROM) +MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED = config.getboolean('mail', 'custom_sender_verification_required', fallback=True) +MAIL_CUSTOM_SENDER_SPF_STRING = config.get('mail', 'custom_sender_spf_string', fallback='') +MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = config.getboolean('mail', 'custom_smtp_allow_private_networks', fallback=False) EMAIL_HOST = config.get('mail', 'host', fallback='localhost') EMAIL_PORT = config.getint('mail', 'port', fallback=25) EMAIL_HOST_USER = config.get('mail', 'user', fallback='') diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index 7908028d9..60a410368 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -21,6 +21,9 @@ td > .form-group > .checkbox { .static-form-row { padding-top: 7px; } +.static-form-row-with-btn { + padding-top: 3px; +} .form-inline .form-control { max-width: 100%; @@ -340,6 +343,7 @@ input[type=number].short { .propagated-settings-box.locked { .propagated-settings-form { opacity: 0.7; + pointer-events: none; } .panel-body.help-text { border-bottom: 1px solid $panel-default-heading-bg; diff --git a/src/setup.py b/src/setup.py index fbf5031b4..5cbd2fe04 100644 --- a/src/setup.py +++ b/src/setup.py @@ -190,6 +190,7 @@ setup( 'django-scopes==1.2.*', 'django-statici18n==2.1.*', 'djangorestframework==3.12.*', + 'dnspython==2.2.*', 'drf_ujson2==1.6.*', 'isoweek', 'jsonschema', diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 71a45c27a..cc9a6db65 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -36,8 +36,11 @@ import datetime import time from decimal import Decimal +from smtplib import SMTPResponseException +import pytest import pytz +from django.test.utils import override_settings from django.utils.timezone import now from django_scopes import scopes_disabled from i18nfield.strings import LazyI18nString @@ -48,6 +51,12 @@ from pretix.base.models import Event, LogEntry, Order, Organizer, Team, User from pretix.testutils.mock import mocker_context +@pytest.fixture +def class_monkeypatch(request, monkeypatch): + request.cls.monkeypatch = monkeypatch + + +@pytest.mark.usefixtures("class_monkeypatch") class EventsTest(SoupTest): @scopes_disabled() def setUp(self): @@ -506,25 +515,204 @@ class EventsTest(SoupTest): assert self.event1.settings.primary_color == self.orga1.settings.primary_color def test_email_settings(self): - with mocker_context() as mocker: - mocked = mocker.patch('pretix.control.views.event.test_custom_smtp_backend') - doc = self.get_doc('/control/event/%s/%s/settings/email' % (self.orga1.slug, self.event1.slug)) - data = extract_form_fields(doc.select("form")[0]) - data['test'] = '1' - doc = self.post_doc('/control/event/%s/%s/settings/email' % (self.orga1.slug, self.event1.slug), - data, follow=True) - print(doc) - assert doc.select('.alert-success') - self.event1.settings.flush() - assert mocked.called + doc = self.get_doc('/control/event/%s/%s/settings/email' % (self.orga1.slug, self.event1.slug)) + data = extract_form_fields(doc.select("form")[0]) + data['mail_from_name'] = 'test' + doc = self.post_doc('/control/event/%s/%s/settings/email' % (self.orga1.slug, self.event1.slug), + data, follow=True) + assert doc.select('.alert-success') + self.event1.settings.flush() + assert self.event1.settings.mail_from_name == "test" + + def test_email_setup_system(self): + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'system' + }, + follow=True + ) + assert doc.select('.alert-success') + self.event1.settings.flush() + assert "mail_from" not in self.orga1.settings._cache() + assert not self.event1.settings.smtp_use_custom + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=True, MAIL_CUSTOM_SENDER_SPF_STRING=False) + def test_email_setup_simple_with_verification(self): + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + self.event1.settings.flush() + assert "mail_from" not in self.event1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + data['verification'] = self.client.session[ + f'sender_mail_verification_code_/control/event/{self.orga1.slug}/{self.event1.slug}/settings/email/setup_test@test.pretix.dev' + ] + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + data, + follow=True + ) + assert doc.select('.alert-success') + self.event1.settings.flush() + assert self.event1.settings.mail_from == 'test@test.pretix.dev' + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=True, MAIL_CUSTOM_SENDER_SPF_STRING=False) + def test_email_setup_simple_with_verification_wrong_code(self): + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + self.event1.settings.flush() + assert "mail_from" not in self.event1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + data['verification'] = 'AAAA' + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + data, + follow=True + ) + assert doc.select('.alert-danger') + self.event1.settings.flush() + assert "mail_from" not in self.event1.settings._cache() + + @staticmethod + def _fake_spf_record(hostname): + return { + 'test.pretix.dev': 'v=spf1 a mx include:level2.pretix.dev ~all', + 'level2.pretix.dev': 'v=spf1 a mx +include:level3.pretix.dev include:spftest.pretix.dev ' + '-include:level4.pretix.dev ~all', + 'level3.pretix.dev': 'v=spf1 a mx include:test2.pretix.dev ~all', + 'level4.pretix.dev': 'v=spf1 a mx include:test3.pretix.dev ~all', + 'test2.pretix.dev': None, + 'test3.pretix.dev': None, + 'spftest.pretix.dev': None, + }[hostname] + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev") + def test_email_setup_no_verification_spf_success(self): + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", EventsTest._fake_spf_record) + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + assert doc.select('.alert-success') + self.event1.settings.flush() + # not yet saved + assert "mail_from" not in self.event1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + data, + follow=True + ) + assert doc.select('.alert-success') + self.event1.settings.flush() + assert self.event1.settings.mail_from == 'test@test.pretix.dev' + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test3.pretix.dev") + def test_email_setup_no_verification_spf_warning(self): + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", EventsTest._fake_spf_record) + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + assert doc.select('.alert-warning') + self.event1.settings.flush() + # not yet saved + assert "mail_from" not in self.event1.settings._cache() + + def test_email_setup_smtp(self): + self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None) + self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8") + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'smtp', + 'smtp-mail_from': 'test@test.pretix.dev', + 'smtp-smtp_host': 'test.pretix.dev', + 'smtp-smtp_port': '587', + }, + follow=True + ) + assert doc.select('.alert-success') + # not yet saved + self.event1.settings.flush() + assert "smtp_use_custom" not in self.event1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + data, + follow=True + ) + assert doc.select('.alert-success') + self.event1.settings.flush() + assert self.event1.settings.mail_from == 'test@test.pretix.dev' + assert self.event1.settings.smtp_host == 'test.pretix.dev' + assert self.event1.settings.smtp_port == 587 + assert self.event1.settings.smtp_use_custom + + def test_email_setup_smtp_failure(self): + def fail(a, b): + raise SMTPResponseException(400, 'Auth denied') + self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", fail) + self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8") + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'smtp', + 'smtp-mail_from': 'test@test.pretix.dev', + 'smtp-smtp_host': 'test.pretix.dev', + 'smtp-smtp_port': '587', + }, + follow=True + ) + assert 'Auth denied' in doc.select('.alert-danger')[0].text + # not yet saved + self.event1.settings.flush() + assert "smtp_use_custom" not in self.event1.settings._cache() + assert "mail_from" not in self.event1.settings._cache() + + def test_email_setup_do_not_allow_private_ip_by_default(self): + doc = self.post_doc( + '/control/event/%s/%s/settings/email/setup' % (self.orga1.slug, self.event1.slug), + { + 'mode': 'simple', + 'smtp-mail_from': 'test@test.pretix.dev', + 'smtp-smtp_host': '10.0.1.1', + 'smtp-smtp_port': '587', + }, + follow=True + ) + assert doc.select('.has-error') + # not yet saved + self.event1.settings.flush() + assert "smtp_use_custom" not in self.event1.settings._cache() + assert "mail_from" not in self.event1.settings._cache() def test_ticket_settings(self): doc = self.get_doc('/control/event/%s/%s/settings/tickets' % (self.orga1.slug, self.event1.slug)) data = extract_form_fields(doc.select("form")[0]) data['ticket_download'] = 'on' data['ticketoutput_testdummy__enabled'] = 'on' - doc = self.post_doc('/control/event/%s/%s/settings/tickets' % (self.orga1.slug, self.event1.slug), - data, follow=True) + self.post_doc('/control/event/%s/%s/settings/tickets' % (self.orga1.slug, self.event1.slug), data, follow=True) self.event1.settings.flush() assert self.event1.settings.get('ticket_download', as_type=bool) diff --git a/src/tests/control/test_organizer.py b/src/tests/control/test_organizer.py index ff8cda330..c2e87e38c 100644 --- a/src/tests/control/test_organizer.py +++ b/src/tests/control/test_organizer.py @@ -20,14 +20,15 @@ # . # import datetime +from smtplib import SMTPResponseException import pytest from django.db import transaction +from django.test.utils import override_settings from django_scopes import scopes_disabled from tests.base import SoupTest, extract_form_fields from pretix.base.models import Event, Organizer, Team, User -from pretix.testutils.mock import mocker_context @pytest.fixture @@ -100,13 +101,194 @@ class OrganizerTest(SoupTest): assert called def test_email_settings(self): - with mocker_context() as mocker: - mocked = mocker.patch('pretix.control.views.organizer.test_custom_smtp_backend') - doc = self.get_doc('/control/organizer/%s/settings/email' % self.orga1.slug) - data = extract_form_fields(doc.select("form")[0]) - data['test'] = '1' - doc = self.post_doc('/control/organizer/%s/settings/email' % self.orga1.slug, - data, follow=True) - assert doc.select('.alert-success') - self.event1.settings.flush() - assert mocked.called + doc = self.get_doc('/control/organizer/%s/settings/email' % self.orga1.slug) + data = extract_form_fields(doc.select("form")[0]) + data['mail_from_name'] = 'test' + doc = self.post_doc('/control/organizer/%s/settings/email' % self.orga1.slug, + data, follow=True) + assert doc.select('.alert-success') + self.orga1.settings.flush() + assert self.orga1.settings.mail_from_name == "test" + + def test_email_setup_system(self): + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'system' + }, + follow=True + ) + assert doc.select('.alert-success') + self.orga1.settings.flush() + assert "mail_from" not in self.orga1.settings._cache() + assert not self.orga1.settings.smtp_use_custom + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=True, MAIL_CUSTOM_SENDER_SPF_STRING=False) + def test_email_setup_simple_with_verification(self): + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + self.orga1.settings.flush() + assert "mail_from" not in self.orga1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + data['verification'] = self.client.session[ + f'sender_mail_verification_code_/control/organizer/{self.orga1.slug}/settings/email/setup_test@test.pretix.dev' + ] + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + data, + follow=True + ) + assert doc.select('.alert-success') + self.orga1.settings.flush() + assert self.orga1.settings.mail_from == 'test@test.pretix.dev' + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=True, MAIL_CUSTOM_SENDER_SPF_STRING=False) + def test_email_setup_simple_with_verification_wrong_code(self): + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + self.orga1.settings.flush() + assert "mail_from" not in self.orga1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + data['verification'] = 'AAAA' + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + data, + follow=True + ) + assert doc.select('.alert-danger') + self.orga1.settings.flush() + assert "mail_from" not in self.orga1.settings._cache() + + @staticmethod + def _fake_spf_record(hostname): + return { + 'test.pretix.dev': 'v=spf1 a mx include:level2.pretix.dev ~all', + 'level2.pretix.dev': 'v=spf1 a mx +include:level3.pretix.dev include:spftest.pretix.dev ' + '-include:level4.pretix.dev ~all', + 'level3.pretix.dev': 'v=spf1 a mx include:test2.pretix.dev ~all', + 'level4.pretix.dev': 'v=spf1 a mx include:test3.pretix.dev ~all', + 'test2.pretix.dev': None, + 'test3.pretix.dev': None, + 'spftest.pretix.dev': None, + }[hostname] + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev") + def test_email_setup_no_verification_spf_success(self): + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record) + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + assert doc.select('.alert-success') + self.orga1.settings.flush() + # not yet saved + assert "mail_from" not in self.orga1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + data, + follow=True + ) + assert doc.select('.alert-success') + self.orga1.settings.flush() + assert self.orga1.settings.mail_from == 'test@test.pretix.dev' + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test3.pretix.dev") + def test_email_setup_no_verification_spf_warning(self): + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record) + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'simple-mail_from': 'test@test.pretix.dev', + }, + follow=True + ) + assert doc.select('.alert-warning') + self.orga1.settings.flush() + # not yet saved + assert "mail_from" not in self.orga1.settings._cache() + + def test_email_setup_smtp(self): + self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None) + self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8") + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'smtp', + 'smtp-mail_from': 'test@test.pretix.dev', + 'smtp-smtp_host': 'test.pretix.dev', + 'smtp-smtp_port': '587', + }, + follow=True + ) + assert doc.select('.alert-success') + # not yet saved + self.orga1.settings.flush() + assert "smtp_use_custom" not in self.orga1.settings._cache() + data = extract_form_fields(doc.select("form")[0]) + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + data, + follow=True + ) + assert doc.select('.alert-success') + self.orga1.settings.flush() + assert self.orga1.settings.mail_from == 'test@test.pretix.dev' + assert self.orga1.settings.smtp_host == 'test.pretix.dev' + assert self.orga1.settings.smtp_port == 587 + assert self.orga1.settings.smtp_use_custom + + def test_email_setup_smtp_failure(self): + def fail(a, b): + raise SMTPResponseException(400, 'Auth denied') + self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", fail) + self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8") + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'smtp', + 'smtp-mail_from': 'test@test.pretix.dev', + 'smtp-smtp_host': 'test.pretix.dev', + 'smtp-smtp_port': '587', + }, + follow=True + ) + assert 'Auth denied' in doc.select('.alert-danger')[0].text + # not yet saved + self.orga1.settings.flush() + assert "smtp_use_custom" not in self.orga1.settings._cache() + assert "mail_from" not in self.orga1.settings._cache() + + def test_email_setup_do_not_allow_private_ip_by_default(self): + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'smtp-mail_from': 'test@test.pretix.dev', + 'smtp-smtp_host': '10.0.1.1', + 'smtp-smtp_port': '587', + }, + follow=True + ) + assert doc.select('.has-error') + # not yet saved + self.orga1.settings.flush() + assert "smtp_use_custom" not in self.orga1.settings._cache() + assert "mail_from" not in self.orga1.settings._cache() diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index c00fd6106..3d59295ae 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -92,6 +92,7 @@ event_urls = [ "settings/payment", "settings/tickets", "settings/email", + "settings/email/setup", "settings/cancel", "settings/invoice", "settings/invoice/preview", @@ -171,6 +172,8 @@ event_urls = [ organizer_urls = [ 'organizer/abc/edit', 'organizer/abc/', + 'organizer/abc/settings/email', + 'organizer/abc/settings/email/setup', 'organizer/abc/teams', 'organizer/abc/team/1/', 'organizer/abc/team/1/edit', @@ -285,6 +288,7 @@ event_permission_urls = [ ("can_change_event_settings", "settings/payment", 200, HTTP_GET), ("can_change_event_settings", "settings/tickets", 200, HTTP_GET), ("can_change_event_settings", "settings/email", 200, HTTP_GET), + ("can_change_event_settings", "settings/email/setup", 200, HTTP_GET), ("can_change_event_settings", "settings/cancel", 200, HTTP_GET), ("can_change_event_settings", "settings/invoice", 200, HTTP_GET), ("can_change_event_settings", "settings/widget", 200, HTTP_GET), @@ -481,6 +485,8 @@ organizer_permission_urls = [ ("can_change_teams", "organizer/dummy/team/1/edit", 200), ("can_change_teams", "organizer/dummy/team/1/delete", 200), ("can_change_organizer_settings", "organizer/dummy/edit", 200), + ("can_change_organizer_settings", "organizer/dummy/settings/email", 200), + ("can_change_organizer_settings", "organizer/dummy/settings/email/setup", 200), ("can_change_organizer_settings", "organizer/dummy/devices", 200), ("can_change_organizer_settings", "organizer/dummy/device/add", 200), ("can_change_organizer_settings", "organizer/dummy/device/1/edit", 404),