Redesign of email settings (#2426)

Co-authored-by: Felix Rindt <felix@rindt.me>
This commit is contained in:
Raphael Michel
2022-01-26 12:47:58 +01:00
committed by GitHub
parent 194042dca5
commit e3c7cd7c6d
27 changed files with 1223 additions and 160 deletions

View File

@@ -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'

View File

@@ -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 <https://pretix.eu/about/en/license>.
#
# 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
# <https://www.gnu.org/licenses/>.
#
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,
)

View File

@@ -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'