diff --git a/src/pretix/control/templates/pretixcontrol/email_setup_simple.html b/src/pretix/control/templates/pretixcontrol/email_setup_simple.html index 6cd9fa2dd..8c600440e 100644 --- a/src/pretix/control/templates/pretixcontrol/email_setup_simple.html +++ b/src/pretix/control/templates/pretixcontrol/email_setup_simple.html @@ -50,6 +50,46 @@ {% endblocktrans %} {% endif %} + {% if dkim_warning %} +
+

+ {{ dkim_warning }} +

+

+ {% trans "Your new DKIM record should be set up as a CNAME record like this:" %} +

+
{{ dkim_hostname }}  CNAME  {{ dkim_cname }}
+

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

+
+ {% elif dkim_cname %} +
+ {% blocktrans trimmed %} + We found a DKIM record on your domain for this system. Great! + {% endblocktrans %} +
+ {% endif %} + {% if dmarc_warning %} +
+

+ {{ dmarc_warning }} +

+

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

+
_dmarc.{{ hostname }}  TXT  "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"
+

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

+
+ {% elif dkim_cname %} +
+ {% blocktrans trimmed %} + We found a DMARC record on your domain for this system. Great! + {% endblocktrans %} +
+ {% endif %} {% if verification %}

{% trans "Verification" %}

@@ -70,7 +110,7 @@ - {% if spf_warning %} + {% if spf_warning or dkim_warning or dmarc_warning %}

{% trans "Cancel" %} diff --git a/src/pretix/control/views/mailsetup.py b/src/pretix/control/views/mailsetup.py index ceaede1ee..63abf3388 100644 --- a/src/pretix/control/views/mailsetup.py +++ b/src/pretix/control/views/mailsetup.py @@ -41,6 +41,30 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm logger = logging.getLogger(__name__) +def get_cname_record(hostname): + try: + r = dns.resolver.Resolver() + answers = r.resolve(hostname, 'CNAME') + answers = list(answers) + if len(answers) != 1: + logger.exception('Found multiple CNAME records for {}'.format(hostname)) + return + return str(answers[0].target).lower() + except: + logger.exception('Could not fetch CNAME record for {}'.format(hostname)) + + +def get_dmarc_record(hostname): + try: + r = dns.resolver.Resolver() + for resp in r.resolve("_dmarc." + hostname, 'TXT'): + data = b''.join(resp.strings).decode() + if 'DMARC1' in data.strip(): + return data + except: + logger.exception("Could not fetch DMARC record for {}".format(hostname)) + + def get_spf_record(hostname): try: r = dns.resolver.Resolver() @@ -49,7 +73,7 @@ def get_spf_record(hostname): if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5 return data except: - logger.exception("Could not fetch SPF record") + logger.exception("Could not fetch SPF record for {}".format(hostname)) def _check_spf_record(not_found_lookup_parts, spf_record, depth): @@ -168,10 +192,15 @@ class MailSettingsSetupView(TemplateView): return super().get(request, *args, **kwargs) session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}' + verify_dns = ( + settings.MAIL_CUSTOM_SENDER_SPF_STRING or + (settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR) or + settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED + ) 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') + (not verify_dns or self.request.POST.get('state') == 'save') ) if allow_save: @@ -192,8 +221,8 @@ class MailSettingsSetupView(TemplateView): spf_warning = None spf_record = None + hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1] 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 = _( @@ -210,7 +239,43 @@ class MailSettingsSetupView(TemplateView): 'this system in the SPF record.' ) - verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning + dkim_warning = None + dkim_hostname = None + dkim_cname = None + if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR: + dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname + cname_target = get_cname_record(dkim_hostname) + dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME + if not dkim_cname.endswith("."): + dkim_cname += "." + if "%s" in dkim_cname: + dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower()) + if not cname_target: + dkim_warning = _( + 'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. ' + 'This means that there is a very high change most of the emails will be rejected or marked as ' + 'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the ' + 'DNS settings at the provider you registered your domain with.' + ) + elif cname_target != dkim_cname: + dkim_warning = _( + 'We found a CNAME record for a DKIM key, but it is not pointing to the right location. ' + '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.' + ) + + dmarc_warning = None + dmarc_record = None + if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED: + dmarc_record = get_dmarc_record(hostname) + if not dmarc_record: + spf_warning = _( + 'We did not find DMARC record for your domain. 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.' + ) + + verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning if verification: if 'verification' in self.request.POST: messages.error(request, _('The verification code was incorrect, please try again.')) @@ -241,6 +306,12 @@ class MailSettingsSetupView(TemplateView): 'spf_warning': spf_warning, 'spf_record': spf_record, 'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING, + 'dkim_warning': dkim_warning, + 'dkim_hostname': dkim_hostname, + 'dkim_cname': dkim_cname, + 'dmarc_warning': dmarc_warning, + 'dmarc_record': dmarc_record, + 'hostname': hostname, 'recp': self.simple_form.cleaned_data.get('mail_from') }, using=self.template_engine, diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 2b49ef69e..a729d4a40 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -262,6 +262,9 @@ MAIL_FROM_NOTIFICATIONS = config.get('mail', 'from_notifications', fallback=MAIL 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_SENDER_DKIM_SELECTOR = config.get('mail', 'custom_sender_dkim_selector', fallback='') +MAIL_CUSTOM_SENDER_DKIM_CNAME = config.get('mail', 'custom_sender_dkim_cname', fallback='') +MAIL_CUSTOM_SENDER_DMARC_REQUIRED = config.getboolean('mail', 'custom_sender_dmarc_required', fallback=False) MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = config.getboolean('mail', 'custom_smtp_allow_private_networks', fallback=DEBUG) EMAIL_HOST = config.get('mail', 'host', fallback='localhost') EMAIL_PORT = config.getint('mail', 'port', fallback=25) diff --git a/src/tests/control/test_organizer.py b/src/tests/control/test_organizer.py index 5ef89e900..5b198b484 100644 --- a/src/tests/control/test_organizer.py +++ b/src/tests/control/test_organizer.py @@ -159,6 +159,22 @@ class OrganizerTest(SoupTest): self.orga1.settings.flush() assert "mail_from" not in self.orga1.settings._cache() + @staticmethod + def _fake_dmarc_record(hostname): + return { + 'test.pretix.dev': 'v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;', + 'bad.pretix.dev': None, + 'none.pretix.dev': None, + }[hostname] + + @staticmethod + def _fake_cname_record(hostname): + return { + 'pretix._domainkey.test.pretix.dev': 'test-pretix-dev.dkim.pretix.eu.', + 'pretix._domainkey.bad.pretix.dev': 'example.org', + 'pretix._domainkey.none.pretix.dev': None, + }[hostname] + @staticmethod def _fake_spf_record(hostname): return { @@ -172,9 +188,17 @@ class OrganizerTest(SoupTest): '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): + @override_settings( + MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, + MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev", + MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix", + MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.", + MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True, + ) + def test_email_setup_no_verification_spf_dmarc_dkim_success(self): self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record) + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record) + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record) doc = self.post_doc( '/control/organizer/%s/settings/email/setup' % self.orga1.slug, { @@ -213,6 +237,43 @@ class OrganizerTest(SoupTest): # not yet saved assert "mail_from" not in self.orga1.settings._cache() + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, + MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix", + MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.", + MAIL_CUSTOM_SENDER_SPF_STRING="") + def test_email_setup_no_verification_dkim_warning(self): + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record) + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'simple-mail_from': 'test@bad.pretix.dev', + }, + follow=True + ) + assert doc.select('.alert-danger') + self.orga1.settings.flush() + # not yet saved + assert "mail_from" not in self.orga1.settings._cache() + + @override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, + MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True, + MAIL_CUSTOM_SENDER_SPF_STRING="") + def test_email_setup_no_verification_dmarc_warning(self): + self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record) + doc = self.post_doc( + '/control/organizer/%s/settings/email/setup' % self.orga1.slug, + { + 'mode': 'simple', + 'simple-mail_from': 'test@bad.pretix.dev', + }, + follow=True + ) + assert doc.select('.alert-danger') + 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")