Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-checkdmarc for openSUSE:Factory checked in at 2025-09-22 16:40:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-checkdmarc (Old) and /work/SRC/openSUSE:Factory/.python-checkdmarc.new.27445 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-checkdmarc" Mon Sep 22 16:40:48 2025 rev:10 rq:1306451 version:5.10.12 Changes: -------- --- /work/SRC/openSUSE:Factory/python-checkdmarc/python-checkdmarc.changes 2025-09-14 18:50:53.815910491 +0200 +++ /work/SRC/openSUSE:Factory/.python-checkdmarc.new.27445/python-checkdmarc.changes 2025-09-22 16:41:41.038463953 +0200 @@ -1,0 +2,15 @@ +Sat Sep 20 08:09:09 UTC 2025 - Martin Hauke <[email protected]> + +- Update to version 5.10.12 + * Proper checking for the start of an SPF record. + * Improve error messages and fix typos (Close issue #182). + * Remove warning when no MX records are found. +- Update to version 5.10.8 + * Return the proper error message when checking an SOA record + for a domain that exist. +- Update to version 5.10.7 + * Set use_signals=False when using timeout decorator to allow it + to be used in multithreaded applications such as web + applications. + +------------------------------------------------------------------- Old: ---- checkdmarc-5.10.6.tar.gz New: ---- checkdmarc-5.10.12.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-checkdmarc.spec ++++++ --- /var/tmp/diff_new_pack.pulwPq/_old 2025-09-22 16:41:41.574486474 +0200 +++ /var/tmp/diff_new_pack.pulwPq/_new 2025-09-22 16:41:41.578486642 +0200 @@ -20,7 +20,7 @@ %bcond_without libalternatives %{?sle15_python_module_pythons} Name: python-checkdmarc -Version: 5.10.6 +Version: 5.10.12 Release: 0 Summary: A Python module and command line parser for SPF and DMARC records License: Apache-2.0 ++++++ checkdmarc-5.10.6.tar.gz -> checkdmarc-5.10.12.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/.github/workflows/python-tests.yaml new/checkdmarc-5.10.12/.github/workflows/python-tests.yaml --- old/checkdmarc-5.10.6/.github/workflows/python-tests.yaml 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/.github/workflows/python-tests.yaml 2025-09-19 22:50:56.000000000 +0200 @@ -30,7 +30,7 @@ make html - name: Check code style run: | - black --check -v . + black --check --diff . - name: Run unit tests run: | coverage run tests.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/.vscode/launch.json new/checkdmarc-5.10.12/.vscode/launch.json --- old/checkdmarc-5.10.6/.vscode/launch.json 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/.vscode/launch.json 2025-09-19 22:50:56.000000000 +0200 @@ -48,6 +48,18 @@ "justMyCode": true }, { + "name": "checkdmarc --skip-tls doesnotexistexample.com", + "type": "debugpy", + "request": "launch", + "module": "checkdmarc._cli", + "args": [ + "--skip-tls", + "doesnotexistexample.com" + ], + "console": "integratedTerminal", + "justMyCode": true + }, + { "name": "checkdmarc --skip-tls dhs.gov", "type": "debugpy", "request": "launch", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/.vscode/settings.json new/checkdmarc-5.10.12/.vscode/settings.json --- old/checkdmarc-5.10.6/.vscode/settings.json 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/.vscode/settings.json 2025-09-19 22:50:56.000000000 +0200 @@ -29,8 +29,10 @@ "fieldlist", "gaierror", "genindex", + "getsizeof", "githubpages", "hostnames", + "Indicatorfor", "Jhozxo", "jppol", "levelname", @@ -60,6 +62,7 @@ "RDATA", "rdatatype", "rdatatypes", + "rdns", "rdtype", "reversename", "rrset", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/CHANGELOG.md new/checkdmarc-5.10.12/CHANGELOG.md --- old/checkdmarc-5.10.6/CHANGELOG.md 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/CHANGELOG.md 2025-09-19 22:50:56.000000000 +0200 @@ -1,6 +1,38 @@ Changelog ========= +5.10.12 +------- + +- Proper checking for the start of an SPF record (PR #184) +- Improve error messages and fix typos (Close issue #182) +- Remove warning when no MX records are found + +5.10.11 +------- + +- Make BIMI error messages clearer + +5.10.10 +------- + +- Add missing periods at the end of BIMI error messages and warnings + +5.10.9 +------ + +- Add periods at the end of error messages to make them nicer for web apps + +5.10.8 +------ + +- Return the proper error message when checking an SOA record for a domain that exist + +5.10.7 +------ + +- Set `use_signals=False` when using timeout decorator to allow it to be used in multithreaded applications such as web applications + 5.10.6 ----- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/_constants.py new/checkdmarc-5.10.12/checkdmarc/_constants.py --- old/checkdmarc-5.10.6/checkdmarc/_constants.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/_constants.py 2025-09-19 22:50:56.000000000 +0200 @@ -18,7 +18,7 @@ See the License for the specific language governing permissions and limitations under the License.""" -__version__ = "5.10.6" +__version__ = "5.10.12" OS = platform.system() OS_RELEASE = platform.release() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/bimi.py new/checkdmarc-5.10.12/checkdmarc/bimi.py --- old/checkdmarc-5.10.6/checkdmarc/bimi.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/bimi.py 2025-09-19 22:50:56.000000000 +0200 @@ -510,7 +510,7 @@ metadata["valid"] = False logging.debug(f"Certificate ValidationError exception: {e_str}") if "all candidates exhausted with no interior errors" in e_str: - e_str = "The certificate was not issued by a recognized Mark Verifying Authority (MVA)" + e_str = "The certificate was not issued by a recognized Mark Verifying Authority (MVA)." validation_errors.append(e_str) not_valid_before_timestamp = vmc.not_valid_before_utc.strftime("%Y-%m-%d %H:%M:%SZ") not_valid_after_timestamp = vmc.not_valid_after_utc.strftime("%Y-%m-%d %H:%M:%SZ") @@ -572,7 +572,7 @@ if required_field not in cert_subject: valid = False validation_errors.append( - f"The the certificate's subject is missing the field {required_field}" + f"The the certificate's subject is missing the required field {required_field}." ) for key in FIELD_REQUIRED_IF_FIELD_IS_MISSING: if key in ["All", mark_type]: @@ -586,7 +586,7 @@ ] if alt_field not in cert_subject: validation_errors.append( - f"{alt_field} is required in the certificate subject if {required_field} is not used in the certificate subject" + f"{alt_field} is required in the certificate subject if {required_field} is not used in the certificate subject." ) valid = False mark_type_fields = ( @@ -606,7 +606,7 @@ for field in other_mark_type_fields: if field in cert_subject: validation_errors.append( - f"The subject {field} is used by {other_mark_type} certificates, not {mark_type} certificates" + f"The subject {field} is used by {other_mark_type} certificates, not {mark_type} certificates." ) valid = False else: @@ -679,7 +679,7 @@ unrelated_records.append(record) if bimi_record_count > 1: - raise MultipleBIMIRecords("Multiple BMI records are not permitted") + raise MultipleBIMIRecords("Multiple BMI records are not permitted.") if len(unrelated_records) > 0: ur_str = "\n\n".join(unrelated_records) raise UnrelatedTXTRecordFoundAtBIMI( @@ -702,12 +702,12 @@ for record in records: if record.startswith(txt_prefix): raise BIMIRecordInWrongLocation( - f"The BIMI record must be located at {target}, not {domain}" + f"The BIMI record must be located at {target}, not {domain}." ) except dns.resolver.NoAnswer: pass except dns.resolver.NXDOMAIN: - raise BIMIRecordNotFound(f"The domain {domain} does not exist") + raise BIMIRecordNotFound(f"The domain {domain} does not exist.") except Exception as error: BIMIRecordNotFound(error) @@ -750,10 +750,11 @@ :exc:`checkdmarc.bimi.MultipleBIMIRecords` """ + domain = normalize_domain(domain) logging.debug(f"Checking for a BIMI record at {selector}._bimi.{domain}") warnings = [] base_domain = get_base_domain(domain) - location = domain.lower() + location = domain record = _query_bimi_record( domain, selector=selector, @@ -767,9 +768,9 @@ ) for root_record in root_records: if root_record.startswith("v=BIMI1"): - warnings.append(f"BIMI record at root of {domain} has no effect") + warnings.append(f"BIMI record at root of {domain} has no effect.") except dns.resolver.NXDOMAIN: - raise BIMIRecordNotFound(f"The domain {domain} does not exist") + raise BIMIRecordNotFound(f"The domain {domain} does not exist.") except dns.exception.DNSException: pass @@ -779,10 +780,16 @@ ) location = base_domain if record is None: - raise BIMIRecordNotFound( - f"A BIMI record does not exist at the {selector} selector for " - f"this domain or its base domain" - ) + if domain == base_domain: + raise BIMIRecordNotFound( + f"A BIMI record does not exist at the {selector} selector for " + f"this domain." + ) + else: + raise BIMIRecordNotFound( + f"A BIMI record does not exist at the {selector} selector for " + "this subdomain or its base domain." + ) return OrderedDict( [("record", record), ("location", location), ("warnings", warnings)] @@ -845,7 +852,7 @@ "should be; most likely, the _bimi " "subdomain record does not actually exist, " "and the request for TXT records was " - "redirected to the base domain" + "redirected to the base domain." ) warnings = [] record = record.strip('"') @@ -878,7 +885,7 @@ tag = pair[0].lower().strip() tag_value = str(pair[1].strip()) if tag not in BIMI_TAGS: - raise InvalidBIMITag(f"{tag} is not a valid BIMI record tag") + raise InvalidBIMITag(f"{tag} is not a valid BIMI record tag.") tags[tag] = OrderedDict(value=tag_value) if include_tag_descriptions: tags[tag]["name"] = BIMI_TAGS[tag]["name"] @@ -898,7 +905,7 @@ svg_metadata = get_svg_metadata(raw_xml) if svg_metadata["width"] != svg_metadata["height"]: warnings.append( - f"It is recommended for BIMI SVG dimensions to be square, not {svg_metadata['width']}x{svg_metadata['height']}" + f"It is recommended for BIMI SVG dimensions to be square, not {svg_metadata['width']}x{svg_metadata['height']}." ) svg_validation_errors = check_svg_requirements(svg_metadata) if len(svg_validation_errors) > 0: @@ -919,7 +926,7 @@ hash_match = True else: warnings.append( - "The image at the l= tag URL does not match the image embedded in the certificate" + "The image at the l= tag URL does not match the image embedded in the certificate." ) except Exception as e: results["certificate"] = dict( @@ -933,7 +940,7 @@ if parsed_dmarc_record and not tags["l"] == "": if not parsed_dmarc_record["valid"]: warnings.append( - "The domain does not have a valid DMARC record. A DMARC policy of quarantine or reject must be in place" + "The domain does not have a valid DMARC record. A DMARC policy of quarantine or reject must be in place." ) else: if parsed_dmarc_record["tags"]["p"]["value"] not in [ @@ -941,23 +948,23 @@ "reject", ]: warnings.append( - "The DMARC policy (p tag) must not be set to quarantine or reject" + "The DMARC policy (p tag) must not be set to quarantine or reject." ) if parsed_dmarc_record["tags"]["sp"]["value"] not in [ "quarantine", "reject", ]: warnings.append( - "The DMARC subdomain policy (sp tag) must be set to quarantine or reject if it is used" + "The DMARC subdomain policy (sp tag) must be set to quarantine or reject if it is used." ) if parsed_dmarc_record["tags"]["pct"]["value"] != 100: warnings.append( - "The DMARC pct tag must be set to 100 (the implicit default) if it is used" + "The DMARC pct tag must be set to 100 (the implicit default) if it is used." ) matching_certificate_provided = hash_match and cert_metadata["valid"] if ("l" in tags and tags["l"]["value"] != "") and not matching_certificate_provided: warnings.append( - "Most email providers will not display a BIMI image without a valid mark certificate" + "Most email providers will not display a BIMI image without a valid mark certificate." ) results["tags"] = tags if svg_metadata is not None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/dmarc.py new/checkdmarc-5.10.12/checkdmarc/dmarc.py --- old/checkdmarc-5.10.6/checkdmarc/dmarc.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/dmarc.py 2025-09-19 22:50:56.000000000 +0200 @@ -224,7 +224,7 @@ ), p=OrderedDict( name="Requested Mail Receiver Policy", - reqired=True, + required=True, description="Specifies the policy to " "be enacted by the " "Receiver at the " @@ -378,7 +378,7 @@ ), v=OrderedDict( name="Version", - reqired=True, + required=True, description="Identifies the record " "retrieved as a DMARC " "record. It MUST have the " @@ -431,7 +431,7 @@ dmarc_records.append(record) elif record.strip().startswith(txt_prefix): raise DMARCRecordStartsWithWhitespace( - "Found a DMARC record that starts with whitespace. " + f"Found a DMARC record at {target} that starts with whitespace. " "Please remove the whitespace, as some implementations " "may not process it correctly." ) @@ -467,12 +467,12 @@ for record in records: if record.startswith(txt_prefix): raise DMARCRecordInWrongLocation( - f"The DMARC record must be located at {target}, not {domain}" + f"The DMARC record must be located at {target}, not {domain}." ) except dns.resolver.NoAnswer: pass except dns.resolver.NXDOMAIN: - raise DMARCRecordNotFound(f"The domain {0} does not exist".format(domain)) + raise DMARCRecordNotFound(f"The domain {0} does not exist.".format(domain)) except Exception as error: raise DMARCRecordNotFound(error) @@ -547,9 +547,9 @@ ) for root_record in root_records: if root_record.startswith("v=DMARC1"): - warnings.append(f"DMARC record at root of {domain} has no effect") + warnings.append(f"DMARC record at root of {domain} has no effect.") except dns.resolver.NXDOMAIN: - raise DMARCRecordNotFound(f"The domain {domain} does not exist") + raise DMARCRecordNotFound(f"The domain {domain} does not exist.") except dns.exception.DNSException: pass @@ -564,7 +564,7 @@ location = base_domain if record is None: raise DMARCRecordNotFound( - "A DMARC record does not exist for this domain or its base domain" + "A DMARC record does not exist for this domain or its base domain." ) return OrderedDict( @@ -860,7 +860,7 @@ "should be; most likely, the _dmarc " "subdomain record does not actually exist, " "and the request for TXT records was " - "redirected to the base domain" + "redirected to the base domain." ) warnings = [] record = record.strip('"') @@ -901,17 +901,17 @@ [("value", dmarc_tags[tag]["default"]), ("explicit", False)] ) if "p" not in tags: - raise DMARCSyntaxError('The record is missing the required policy ("p") tag') + raise DMARCSyntaxError('The record is missing the required policy ("p") tag.') tags["p"]["value"] = tags["p"]["value"].lower() if "sp" not in tags: tags["sp"] = OrderedDict([("value", tags["p"]["value"]), ("explicit", False)]) if list(tags.keys())[1] != "p": - raise DMARCSyntaxError("the p tag must immediately follow the v tag") + raise DMARCSyntaxError("the p tag must immediately follow the v tag.") tags["v"]["value"] = tags["v"]["value"].upper() # Validate tag values for tag in tags: if tag not in dmarc_tags: - raise InvalidDMARCTag(f"{tag} is not a valid DMARC tag") + raise InvalidDMARCTag(f"{tag} is not a valid DMARC tag.") tag_value = tags[tag]["value"] allowed_values = None explicit = tags[tag]["explicit"] @@ -929,19 +929,19 @@ tag_value = tag_value.split(":") if "0" in tag_value and "1" in tag_value: warnings.append( - "When 1 is present in the fo tag, including 0 is redundant" + "When 1 is present in the fo tag, including in the fo tag 0 is redundant." ) for value in tag_value: if value not in allowed_values: raise InvalidDMARCTagValue( - f"{value} is not a valid option for the DMARC fo tag" + f"{value} is not a valid option for the DMARC fo tag." ) elif tag == "rf": tag_value = tag_value.lower().split(":") for value in tag_value: if value not in allowed_values: raise InvalidDMARCTagValue( - f"{value} is not a valid option for the DMARC rf tag" + f"{value} is not a valid option for the DMARC rf tag." ) elif allowed_values and tag_value not in allowed_values: @@ -954,12 +954,12 @@ try: tags["pct"]["value"] = int(tags["pct"]["value"]) except ValueError: - raise InvalidDMARCTagValue("The value of the pct tag must be an integer") + raise InvalidDMARCTagValue("The value of the pct tag must be an integer.") try: tags["ri"]["value"] = int(tags["ri"]["value"]) except ValueError: - raise InvalidDMARCTagValue("The value of the ri tag must be an integer") + raise InvalidDMARCTagValue("The value of the ri tag must be an integer.") if "rua" in tags: parsed_uris = [] @@ -971,7 +971,7 @@ email_address = uri["address"] if uri["size_limit"]: warnings.append( - f"Setting a size limit on rua reports sent to {email_address} could cause incomplete reporting" + f"Setting a size limit on rua reports sent to {email_address} could cause incomplete reporting." ) email_domain = email_address.split("@")[-1] if email_domain.lower() != domain: @@ -993,7 +993,7 @@ if len(hosts) == 0: raise DMARCReportEmailAddressMissingMXRecords( "The domain for rua email address " - f"{email_address} has no MX records" + f"{email_address} has no MX records." ) except DNSException as warning: raise DMARCReportEmailAddressMissingMXRecords( @@ -1009,7 +1009,7 @@ warnings.append( str( _DMARCBestPracticeWarning( - "Some DMARC reporters might not send to more than two rua URIs" + "Some DMARC reporters might not send to more than two rua URIs." ) ) ) @@ -1017,7 +1017,7 @@ warnings.append( str( _DMARCBestPracticeWarning( - "rua tag (destination for aggregate reports) not found" + "rua tag (destination for aggregate reports) not found." ) ) ) @@ -1032,7 +1032,7 @@ email_address = uri["address"] if uri["size_limit"]: warnings.append( - f"Setting a size limit on ruf reports sent to {email_address} could cause incomplete reporting" + f"Setting a size limit on ruf reports sent to {email_address} could cause incomplete reporting." ) email_domain = email_address.split("@")[-1] if email_domain.lower() != domain: @@ -1071,30 +1071,30 @@ warnings.append( str( _DMARCBestPracticeWarning( - "Some DMARC reporters might not send to more than two ruf URIs" + "Some DMARC reporters might not send to more than two ruf URIs." ) ) ) if tags["pct"]["value"] < 0 or tags["pct"]["value"] > 100: warnings.append( - str(InvalidDMARCTagValue("pct value must be an integer between 0 and 100")) + str(InvalidDMARCTagValue("pct value must be an integer between 0 and 100.")) ) elif tags["pct"]["value"] == 0: - warnings.append("A pct value of 0 disables DMARC enforcement") + warnings.append("A pct value of 0 disables DMARC enforcement.") elif tags["pct"]["value"] < 100: warning_msg = ( "pct value is less than 100. This leads to " "inconsistent and unpredictable policy " "enforcement. Consider using p=none to " - "monitor results instead" + "monitor results instead." ) warnings.append(str(_DMARCBestPracticeWarning(warning_msg))) if parked and tags["p"]["value"] != "reject": - warning_msg = "Policy (p=) should be reject for parked domains" + warning_msg = "Policy (p=) should be reject for parked domains." warnings.append(str(_DMARCBestPracticeWarning(warning_msg))) if parked and tags["sp"]["value"] != "reject": - warning_msg = "Subdomain policy (sp=) should be reject for parked domains" + warning_msg = "Subdomain policy (sp=) should be reject for parked domains." warnings.append(str(_DMARCBestPracticeWarning(warning_msg))) # Add descriptions if requested diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/mta_sts.py new/checkdmarc-5.10.12/checkdmarc/mta_sts.py --- old/checkdmarc-5.10.6/checkdmarc/mta_sts.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/mta_sts.py 2025-09-19 22:50:56.000000000 +0200 @@ -187,7 +187,7 @@ unrelated_records.append(record) if sts_record_count > 1: - raise MultipleMTASTSRecords("Multiple MTA-STS records are not permitted") + raise MultipleMTASTSRecords("Multiple MTA-STS records are not permitted.") if len(unrelated_records) > 0: ur_str = "\n\n".join(unrelated_records) raise UnrelatedTXTRecordFoundAtMTASTS( @@ -210,12 +210,12 @@ for record in records: if record.startswith(txt_prefix): raise MTASTSRecordInWrongLocation( - f"The MTA-STS record must be located at {target}, not {domain}" + f"The MTA-STS record must be located at {target}, not {domain}." ) except dns.resolver.NoAnswer: pass except dns.resolver.NXDOMAIN: - raise MTASTSRecordNotFound(f"The domain {domain} does not exist") + raise MTASTSRecordNotFound(f"The domain {domain} does not exist.") except Exception as error: raise MTASTSRecordNotFound(error) except Exception as error: @@ -223,7 +223,7 @@ if sts_record is None: raise MTASTSRecordNotFound( - "An MTA-STS DNS record does not exist for this domain" + "An MTA-STS DNS record does not exist for this domain." ) return OrderedDict([("record", sts_record), ("warnings", warnings)]) @@ -301,7 +301,7 @@ tag = pair[0].lower().strip() tag_value = str(pair[1].strip()) if tag not in mta_sts_tags: - raise InvalidMTASTSTag(f"{tag} is not a valid MTA-STS record tag") + raise InvalidMTASTSTag(f"{tag} is not a valid MTA-STS record tag.") tags[tag] = OrderedDict(value=tag_value) if include_tag_descriptions: tags[tag]["description"] = mta_sts_tags[tag]["description"] @@ -381,7 +381,7 @@ acceptable_keys = required_keys.copy() acceptable_keys.append("mx") if "\n" in policy and "\r\n" not in policy: - warnings.append("MTA-STS policy lines should end with CRLF not LF") + warnings.append("MTA-STS policy lines should end with CRLF not LF.") policy = policy.replace("\n", "\r\n") lines = policy.split("\r\n") for i in range(len(lines)): @@ -390,7 +390,7 @@ continue key_value = lines[i].split(":") if len(key_value) != 2: - raise MTASTSPolicySyntaxError(f"Line {line}: Not a key: value pair") + raise MTASTSPolicySyntaxError(f"Line {line}: Not a key: value pair.") key = key_value[0].strip() value = key_value[1].strip() if key not in acceptable_keys: @@ -402,7 +402,7 @@ elif key == "mode" and value not in modes: MTASTSPolicySyntaxError(f"Line {line}: Invalid mode: {value}") elif key == "max_age": - error_msg = "max_age must be an integer value between 0 and 31557600" + error_msg = "max_age must be an integer value between 0 and 31557600." if "." in value: raise MTASTSPolicySyntaxError(error_msg) try: @@ -419,11 +419,11 @@ mx.append(value) for required_key in required_keys: if required_key not in parsed_policy: - raise MTASTSPolicySyntaxError(f"Missing required key: {required_key}") + raise MTASTSPolicySyntaxError(f"Missing required key: {required_key}.") if parsed_policy["mode"] != "none" and len(mx) == 0: raise MTASTSPolicySyntaxError( - f"{parsed_policy['mode']} mode requires at least one mx value" + f"{parsed_policy['mode']} mode requires at least one mx value." ) parsed_policy["mx"] = mx diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/smtp.py new/checkdmarc-5.10.12/checkdmarc/smtp.py --- old/checkdmarc-5.10.6/checkdmarc/smtp.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/smtp.py 2025-09-19 22:50:56.000000000 +0200 @@ -48,7 +48,10 @@ @timeout_decorator.timeout( - 5, timeout_exception=SMTPError, exception_message="Connection timed out" + 5, + timeout_exception=SMTPError, + exception_message="Connection timed out", + use_signals=False, ) def test_tls( hostname: str, *, ssl_context: ssl.SSLContext = None, cache: ExpiringDict = None @@ -162,7 +165,10 @@ @timeout_decorator.timeout( - 5, timeout_exception=SMTPError, exception_message="Connection timed out" + 5, + timeout_exception=SMTPError, + exception_message="Connection timed out", + use_signals=False, ) def test_starttls( hostname: str, *, ssl_context: ssl.SSLContext = None, cache: ExpiringDict = None @@ -333,8 +339,6 @@ ) if parked and len(hosts) > 0: warnings.append("MX records found on parked domains") - elif not parked and len(hosts) == 0: - warnings.append("No MX records found. Is the domain parked?") if approved_hostnames: approved_hostnames = list(map(lambda h: h.lower(), approved_hostnames)) @@ -376,7 +380,7 @@ if len(tlsa_records) > 0: host["tlsa"] = tlsa_records if len(host["addresses"]) == 0: - warnings.append(f"{hostname} does not have any A or AAAA DNS records") + warnings.append(f"{hostname} does not have any A or AAAA DNS records.") except Exception as e: if hostname.lower().endswith(".msv1.invalid"): warnings.append( @@ -422,11 +426,11 @@ starttls = test_starttls(hostname, cache=STARTTLS_CACHE) tls = starttls if not starttls: - warnings.append(f"STARTTLS is not supported on {hostname}") + warnings.append(f"STARTTLS is not supported on {hostname}.") tls = test_tls(hostname, cache=TLS_CACHE) if not tls: - warnings.append(f"SSL/TLS is not supported on {hostname}") + warnings.append(f"SSL/TLS is not supported on {hostname}.") host["tls"] = tls host["starttls"] = starttls except DNSException as warning: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/smtp_tls_reporting.py new/checkdmarc-5.10.12/checkdmarc/smtp_tls_reporting.py --- old/checkdmarc-5.10.6/checkdmarc/smtp_tls_reporting.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/smtp_tls_reporting.py 2025-09-19 22:50:56.000000000 +0200 @@ -183,7 +183,7 @@ if sts_record_count > 1: raise MultipleSMTPTLSReportingRecords( - "Multiple SMTP TLS Reporting records are not permitted" + "Multiple SMTP TLS Reporting records are not permitted." ) if len(unrelated_records) > 0: ur_str = "\n\n".join(unrelated_records) @@ -213,7 +213,7 @@ except dns.resolver.NoAnswer: pass except dns.resolver.NXDOMAIN: - raise SMTPTLSReportingRecordNotFound(f"The domain {domain} does not exist") + raise SMTPTLSReportingRecordNotFound(f"The domain {domain} does not exist.") except Exception as error: raise SMTPTLSReportingRecordNotFound(error) except Exception as error: @@ -221,7 +221,7 @@ if sts_record is None: raise SMTPTLSReportingRecordNotFound( - "An SMTP TLS Reporting DNS record does not exist for this domain" + "An SMTP TLS Reporting DNS record does not exist for this domain." ) return OrderedDict([("record", sts_record), ("warnings", warnings)]) @@ -300,18 +300,18 @@ tag_value = str(pair[1].strip()) if tag not in smtp_rpt_tags: raise InvalidSMTPTLSReportingTag( - f"{tag} is not a valid SMTP TLS Reporting record tag" + f"{tag} is not a valid SMTP TLS Reporting record tag." ) tags[tag] = OrderedDict(value=tag_value) if include_tag_descriptions: tags[tag]["description"] = smtp_rpt_tags[tag]["description"] if "rua" not in tags: - SMTPTLSReportingSyntaxError("The record is missing the required rua tag") + SMTPTLSReportingSyntaxError("The record is missing the required rua tag.") tags["rua"]["value"] = tags["rua"]["value"].split(",") for uri in tags["rua"]["value"]: if len(SMTPTLSREPORTING_URI_REGEX.findall(uri)) != 1: raise SMTPTLSReportingSyntaxError( - f"{uri} is not a valid SMTP TLS reporting URI" + f"{uri} is not a valid SMTP TLS reporting URI." ) return OrderedDict(tags=tags, warnings=warnings) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/soa.py new/checkdmarc-5.10.12/checkdmarc/soa.py --- old/checkdmarc-5.10.6/checkdmarc/soa.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/soa.py 2025-09-19 22:50:56.000000000 +0200 @@ -33,7 +33,7 @@ Parses a raw SOA record string and returns an OrderedDict with validated fields. """ if not isinstance(rr, str) or not rr.strip(): - raise ValueError("SOA rrdata must be a non-empty string") + raise ValueError("SOA rrdata must be a non-empty string.") tokens = rr.strip().split() if len(tokens) != 7: @@ -101,6 +101,7 @@ results = OrderedDict([("record", record)]) except Exception as e: results = OrderedDict([("error", str(e))]) + return results try: results["values"] = parse_soa_string(record) except Exception as e: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/spf.py new/checkdmarc-5.10.12/checkdmarc/spf.py --- old/checkdmarc-5.10.6/checkdmarc/spf.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/spf.py 2025-09-19 22:50:56.000000000 +0200 @@ -186,20 +186,36 @@ for record in answers: if record == "Undecodable characters": raise UndecodableCharactersInTXTRecord( - f"A TXT record at {domain} contains undecodable characters" + f"A TXT record at {domain} contains undecodable characters." ) - if record.startswith(txt_prefix): + # https://datatracker.ietf.org/doc/html/rfc7208#section-4.5 + # + # Starting with the set of records that were returned by the lookup, + # discard records that do not begin with a version section of exactly + # "v=spf1". Note that the version section is terminated by either an + # SP character or the end of the record. As an example, a record with + # a version section of "v=spf10" does not match and is discarded. + if record.startswith(f"{txt_prefix} ") or record == txt_prefix: spf_txt_records.append(record) + elif record.startswith(txt_prefix): + raise SPFRecordNotFound( + "According to RFC7208 section 4.5, a SPF record should be" + f" equal to {txt_prefix} or begin with {txt_prefix} " + "followed by a space.", + domain, + ) if len(spf_txt_records) > 1: raise MultipleSPFRTXTRecords(f"{domain} has multiple SPF TXT records") elif len(spf_txt_records) == 1: spf_record = spf_txt_records[0] if spf_record is None: - raise SPFRecordNotFound(f"{domain} does not have a SPF TXT record", domain) + raise SPFRecordNotFound(f"{domain} does not have a SPF TXT record.", domain) except dns.resolver.NoAnswer: - raise SPFRecordNotFound(f"{domain} does not have a SPF TXT record", domain) + raise SPFRecordNotFound(f"{domain} does not have a SPF TXT record.", domain) except dns.resolver.NXDOMAIN: - raise SPFRecordNotFound(f"The domain {domain} does not exist", domain) + raise SPFRecordNotFound(f"The domain {domain} does not exist.", domain) + except SPFRecordNotFound as error: + raise error except Exception as error: raise SPFRecordNotFound(error, domain) @@ -266,7 +282,7 @@ f"{correct_record} not: {record}" ) if len(AFTER_ALL_REGEX.findall(record)) > 0: - warnings.append("Any text after the all mechanism is ignored") + warnings.append("Any text after the all mechanism is ignored.") record = AFTER_ALL_REGEX.sub(r"\1", record) parsed_record = spf_syntax_checker.parse(record) if not parsed_record.is_valid: @@ -321,20 +337,20 @@ ipaddress.ip_network(value, strict=False), ipaddress.IPv4Network ): raise SPFSyntaxError( - f"{value} is not a valid ipv4 value. Looks like ipv6" + f"{value} is not a valid ipv4 value. Looks like ipv6." ) except ValueError: - raise SPFSyntaxError(f"{value} is not a valid ipv4 value") + raise SPFSyntaxError(f"{value} is not a valid ipv4 value.") elif mechanism == "ip6": try: if not isinstance( ipaddress.ip_network(value, strict=False), ipaddress.IPv6Network ): raise SPFSyntaxError( - f"{value} is not a valid ipv6 value. Looks like ipv4" + f"{value} is not a valid ipv6 value. Looks like ipv4." ) except ValueError: - raise SPFSyntaxError(f"{value} is not a valid ipv6 value") + raise SPFSyntaxError(f"{value} is not a valid ipv6 value.") if mechanism == "a": if value == "": @@ -521,18 +537,18 @@ for token in tokens: if token not in ["all", "e", "f", "s", "n"]: raise SPFSyntaxError( - f"{token} is not a valid token for the rr tag" + f"{token} is not a valid token for the rr tag." ) parsed["rr"] = result elif mechanism == "rp": if not value.isdigit(): raise SPFSyntaxError( - f"{value} is not a valid ra tag value - should be a number" + f"{value} is not a valid ra tag value - should be a number." ) if int(value) < 0 or int(value) > 100: raise SPFSyntaxError( - f"{value} is not a valid ra tag value - should be a number between 0 and 100" + f"{value} is not a valid ra tag value - should be a number between 0 and 100." ) parsed["rp"] = result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/checkdmarc-5.10.6/checkdmarc/utils.py new/checkdmarc-5.10.12/checkdmarc/utils.py --- old/checkdmarc-5.10.6/checkdmarc/utils.py 2025-09-12 19:54:42.000000000 +0200 +++ new/checkdmarc-5.10.12/checkdmarc/utils.py 2025-09-19 22:50:56.000000000 +0200 @@ -193,7 +193,7 @@ domain, qt, nameservers=nameservers, resolver=resolver, timeout=timeout ) except dns.resolver.NXDOMAIN: - raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist") + raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist.") except dns.resolver.NoAnswer: # Sometimes a domain will only have A or AAAA records, but not both pass @@ -271,9 +271,9 @@ domain, "TXT", nameservers=nameservers, resolver=resolver, timeout=timeout ) except dns.resolver.NXDOMAIN: - raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist") + raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist.") except dns.resolver.NoAnswer: - raise DNSException(f"The domain {domain} does not have any TXT records") + raise DNSException(f"The domain {domain} does not have any TXT records.") except Exception as error: raise DNSException(error) @@ -309,9 +309,9 @@ domain, "SOA", nameservers=nameservers, resolver=resolver, timeout=timeout )[0] except dns.resolver.NXDOMAIN: - raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist") + raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist.") except dns.resolver.NoAnswer: - raise DNSException(f"The domain {domain} does not have an SOA record") + raise DNSException(f"The domain {domain} does not have an SOA record.") except Exception as error: raise DNSException(error) @@ -351,7 +351,7 @@ domain, "NS", nameservers=nameservers, resolver=resolver, timeout=timeout ) except dns.resolver.NXDOMAIN: - raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist") + raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist.") except dns.resolver.NoAnswer: pass except Exception as error: @@ -415,7 +415,7 @@ ) hosts = sorted(hosts, key=lambda h: (h["preference"], h["hostname"])) except dns.resolver.NXDOMAIN: - raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist") + raise DNSExceptionNXDOMAIN(f"The domain {domain} does not exist.") except dns.resolver.NoAnswer: pass except Exception as error:
