2024-09-18 12:12:42 +02:00
|
|
|
from dataclasses import dataclass
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
from app.config import (
|
|
|
|
EMAIL_SERVERS_WITH_PRIORITY,
|
|
|
|
EMAIL_DOMAIN,
|
|
|
|
PARTNER_DOMAINS,
|
|
|
|
PARTNER_DOMAIN_VALIDATION_PREFIXES,
|
|
|
|
)
|
2024-09-13 14:49:48 +02:00
|
|
|
from app.constants import DMARC_RECORD
|
2022-10-11 07:17:37 +02:00
|
|
|
from app.db import Session
|
2024-09-13 14:49:48 +02:00
|
|
|
from app.dns_utils import (
|
2024-09-17 16:15:10 +02:00
|
|
|
DNSClient,
|
2024-09-13 14:49:48 +02:00
|
|
|
is_mx_equivalent,
|
2024-09-17 16:15:10 +02:00
|
|
|
get_network_dns_client,
|
2024-09-13 14:49:48 +02:00
|
|
|
)
|
2022-10-11 07:17:37 +02:00
|
|
|
from app.models import CustomDomain
|
2024-09-13 14:49:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class DomainValidationResult:
|
|
|
|
success: bool
|
|
|
|
errors: [str]
|
2022-10-11 07:17:37 +02:00
|
|
|
|
|
|
|
|
|
|
|
class CustomDomainValidation:
|
2024-09-17 16:15:10 +02:00
|
|
|
def __init__(
|
2024-09-18 12:12:42 +02:00
|
|
|
self,
|
|
|
|
dkim_domain: str,
|
|
|
|
dns_client: DNSClient = get_network_dns_client(),
|
|
|
|
partner_domains: Optional[dict[int, str]] = None,
|
|
|
|
partner_domains_validation_prefixes: Optional[dict[int, str]] = None,
|
2024-09-17 16:15:10 +02:00
|
|
|
):
|
2022-10-11 07:17:37 +02:00
|
|
|
self.dkim_domain = dkim_domain
|
2024-09-17 16:15:10 +02:00
|
|
|
self._dns_client = dns_client
|
2024-09-18 12:12:42 +02:00
|
|
|
self._partner_domains = partner_domains or PARTNER_DOMAINS
|
|
|
|
self._partner_domain_validation_prefixes = (
|
|
|
|
partner_domains_validation_prefixes or PARTNER_DOMAIN_VALIDATION_PREFIXES
|
|
|
|
)
|
2022-10-11 07:17:37 +02:00
|
|
|
|
2024-09-18 12:12:42 +02:00
|
|
|
def get_ownership_verification_record(self, domain: CustomDomain) -> str:
|
|
|
|
prefix = "sl-verification"
|
|
|
|
if (
|
|
|
|
domain.partner_id is not None
|
|
|
|
and domain.partner_id in self._partner_domain_validation_prefixes
|
|
|
|
):
|
|
|
|
prefix = self._partner_domain_validation_prefixes[domain.partner_id]
|
|
|
|
return f"{prefix}={domain.ownership_txt_token}"
|
2022-10-11 07:17:37 +02:00
|
|
|
|
2024-09-18 12:12:42 +02:00
|
|
|
def get_dkim_records(self, domain: CustomDomain) -> {str: str}:
|
|
|
|
"""
|
|
|
|
Get a list of dkim records to set up. Depending on the custom_domain, whether if it's from a partner or not,
|
|
|
|
it will return the default ones or the partner ones.
|
2022-10-11 07:17:37 +02:00
|
|
|
"""
|
2024-09-18 12:12:42 +02:00
|
|
|
|
|
|
|
# By default use the default domain
|
|
|
|
dkim_domain = self.dkim_domain
|
|
|
|
if domain.partner_id is not None:
|
|
|
|
# Domain is from a partner. Retrieve the partner config and use that domain if exists
|
|
|
|
partner_domain = self._partner_domains.get(domain.partner_id)
|
|
|
|
if partner_domain is not None:
|
|
|
|
dkim_domain = partner_domain
|
|
|
|
|
|
|
|
return {
|
|
|
|
f"{key}._domainkey": f"{key}._domainkey.{dkim_domain}"
|
|
|
|
for key in ("dkim", "dkim02", "dkim03")
|
|
|
|
}
|
2022-10-11 07:17:37 +02:00
|
|
|
|
|
|
|
def validate_dkim_records(self, custom_domain: CustomDomain) -> dict[str, str]:
|
|
|
|
"""
|
|
|
|
Check if dkim records are properly set for this custom domain.
|
|
|
|
Returns empty list if all records are ok. Other-wise return the records that aren't properly configured
|
|
|
|
"""
|
2024-09-17 16:15:10 +02:00
|
|
|
correct_records = {}
|
2022-10-11 07:17:37 +02:00
|
|
|
invalid_records = {}
|
2024-09-18 12:12:42 +02:00
|
|
|
expected_records = self.get_dkim_records(custom_domain)
|
2024-09-17 16:15:10 +02:00
|
|
|
for prefix, expected_record in expected_records.items():
|
2022-10-11 07:17:37 +02:00
|
|
|
custom_record = f"{prefix}.{custom_domain.domain}"
|
2024-09-17 16:15:10 +02:00
|
|
|
dkim_record = self._dns_client.get_cname_record(custom_record)
|
|
|
|
if dkim_record == expected_record:
|
|
|
|
correct_records[prefix] = custom_record
|
|
|
|
else:
|
2022-10-11 07:17:37 +02:00
|
|
|
invalid_records[custom_record] = dkim_record or "empty"
|
2024-09-17 16:15:10 +02:00
|
|
|
|
|
|
|
# HACK
|
|
|
|
# As initially we only had one dkim record, we want to allow users that had only the original dkim record and
|
|
|
|
# the domain validated to continue seeing it as validated (although showing them the missing records).
|
|
|
|
# However, if not even the original dkim record is right, even if the domain was dkim_verified in the past,
|
|
|
|
# we will remove the dkim_verified flag.
|
|
|
|
# This is done in order to give users with the old dkim config (only one) to update their CNAMEs
|
2022-10-11 07:17:37 +02:00
|
|
|
if custom_domain.dkim_verified:
|
2024-09-17 16:15:10 +02:00
|
|
|
# Check if at least the original dkim is there
|
|
|
|
if correct_records.get("dkim._domainkey") is not None:
|
|
|
|
# Original dkim record is there. Return the missing records (if any) and don't clear the flag
|
|
|
|
return invalid_records
|
|
|
|
|
|
|
|
# Original DKIM record is not there, which means the DKIM config is not finished. Proceed with the
|
|
|
|
# rest of the code path, returning the invalid records and clearing the flag
|
2022-10-11 07:17:37 +02:00
|
|
|
custom_domain.dkim_verified = len(invalid_records) == 0
|
|
|
|
Session.commit()
|
|
|
|
return invalid_records
|
2024-09-13 14:49:48 +02:00
|
|
|
|
|
|
|
def validate_domain_ownership(
|
|
|
|
self, custom_domain: CustomDomain
|
|
|
|
) -> DomainValidationResult:
|
|
|
|
"""
|
|
|
|
Check if the custom_domain has added the ownership verification records
|
|
|
|
"""
|
2024-09-17 16:15:10 +02:00
|
|
|
txt_records = self._dns_client.get_txt_record(custom_domain.domain)
|
2024-09-18 12:12:42 +02:00
|
|
|
expected_verification_record = self.get_ownership_verification_record(
|
|
|
|
custom_domain
|
|
|
|
)
|
2024-09-13 14:49:48 +02:00
|
|
|
|
2024-09-18 12:12:42 +02:00
|
|
|
if expected_verification_record in txt_records:
|
2024-09-13 14:49:48 +02:00
|
|
|
custom_domain.ownership_verified = True
|
|
|
|
Session.commit()
|
|
|
|
return DomainValidationResult(success=True, errors=[])
|
|
|
|
else:
|
|
|
|
return DomainValidationResult(success=False, errors=txt_records)
|
|
|
|
|
|
|
|
def validate_mx_records(
|
|
|
|
self, custom_domain: CustomDomain
|
|
|
|
) -> DomainValidationResult:
|
2024-09-17 16:15:10 +02:00
|
|
|
mx_domains = self._dns_client.get_mx_domains(custom_domain.domain)
|
2024-09-13 14:49:48 +02:00
|
|
|
|
|
|
|
if not is_mx_equivalent(mx_domains, EMAIL_SERVERS_WITH_PRIORITY):
|
|
|
|
return DomainValidationResult(
|
|
|
|
success=False,
|
|
|
|
errors=[f"{priority} {domain}" for (priority, domain) in mx_domains],
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
custom_domain.verified = True
|
|
|
|
Session.commit()
|
|
|
|
return DomainValidationResult(success=True, errors=[])
|
|
|
|
|
|
|
|
def validate_spf_records(
|
|
|
|
self, custom_domain: CustomDomain
|
|
|
|
) -> DomainValidationResult:
|
2024-09-17 16:15:10 +02:00
|
|
|
spf_domains = self._dns_client.get_spf_domain(custom_domain.domain)
|
2024-09-13 14:49:48 +02:00
|
|
|
if EMAIL_DOMAIN in spf_domains:
|
|
|
|
custom_domain.spf_verified = True
|
|
|
|
Session.commit()
|
|
|
|
return DomainValidationResult(success=True, errors=[])
|
|
|
|
else:
|
|
|
|
custom_domain.spf_verified = False
|
|
|
|
Session.commit()
|
|
|
|
return DomainValidationResult(
|
2024-09-17 16:15:10 +02:00
|
|
|
success=False,
|
|
|
|
errors=self._dns_client.get_txt_record(custom_domain.domain),
|
2024-09-13 14:49:48 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
def validate_dmarc_records(
|
|
|
|
self, custom_domain: CustomDomain
|
|
|
|
) -> DomainValidationResult:
|
2024-09-17 16:15:10 +02:00
|
|
|
txt_records = self._dns_client.get_txt_record("_dmarc." + custom_domain.domain)
|
2024-09-13 14:49:48 +02:00
|
|
|
if DMARC_RECORD in txt_records:
|
|
|
|
custom_domain.dmarc_verified = True
|
|
|
|
Session.commit()
|
|
|
|
return DomainValidationResult(success=True, errors=[])
|
|
|
|
else:
|
|
|
|
custom_domain.dmarc_verified = False
|
|
|
|
Session.commit()
|
|
|
|
return DomainValidationResult(success=False, errors=txt_records)
|