From e1736e8d2a75f64dbe7b6264124497d7ee9c2be9 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 3 Jul 2026 11:57:00 +0200 Subject: [PATCH] Mail setup: Add DKIM + DMARC validation --- .../pretixcontrol/email_setup_simple.html | 42 ++++++++++- src/pretix/control/views/mailsetup.py | 72 ++++++++++++++++++- src/pretix/settings.py | 3 + 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/pretix/control/templates/pretixcontrol/email_setup_simple.html b/src/pretix/control/templates/pretixcontrol/email_setup_simple.html index 6cd9fa2ddc..8c600440ec 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 6ef1f70fb6..97badc4810 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): @@ -192,8 +216,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 +234,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 != settings.MAIL_CUSTOM_SENDER_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.')) @@ -240,6 +300,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 3cbdf4ff12..f443e8820d 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -257,6 +257,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)