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:

Reply via email to