From 9b88b44f6eec5eb02f3f295bed968c0d3daeb53d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:24:08 +0000 Subject: [PATCH 1/3] Initial plan From 37917480fbcfdb03069535099e6659112b0d7765 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:33:07 +0000 Subject: [PATCH 2/3] Fix: Treat undecodable characters in non-SPF TXT records as warnings - Modified spf.py to check for undecodable characters and add warning instead of raising exception - Fixed get_spf_record to preserve warnings from query_spf_record - Added test case for undecodable characters in non-SPF records Co-authored-by: seanthegeek <44679+seanthegeek@users.noreply.github.com> --- checkdmarc/spf.py | 24 ++++++++++++++++++------ tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/checkdmarc/spf.py b/checkdmarc/spf.py index d1a922a..3ff44ac 100644 --- a/checkdmarc/spf.py +++ b/checkdmarc/spf.py @@ -437,10 +437,6 @@ def query_spf_record( ) spf_record = None for record in answers: - if record == "Undecodable characters": - raise UndecodableCharactersInTXTRecord( - "A TXT record contains undecodable characters." - ) # https://datatracker.ietf.org/doc/html/rfc7208#section-4.5 # # Starting with the set of records that were returned by the lookup, @@ -448,6 +444,15 @@ def query_spf_record( # "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. + + # Check for undecodable characters + if record == "Undecodable characters": + # We can't determine if this is an SPF record, so treat it as a warning + warnings.append( + "A non-SPF TXT record contains undecodable characters and was ignored." + ) + continue + if record.strip('"').startswith(txt_prefix): spf_txt_records.append(record) elif record.startswith(txt_prefix): @@ -1235,14 +1240,16 @@ def get_spf_record( :exc:`checkdmarc.spf.SPFTooManyDNSLookups` """ domain = normalize_domain(domain) - record = query_spf_record( + query_result = query_spf_record( domain, nameservers=nameservers, resolver=resolver, timeout=timeout, timeout_retries=timeout_retries, ) - record = record["record"] + record = query_result["record"] + query_warnings = query_result.get("warnings", []) + parsed_record = parse_spf_record( record, domain, @@ -1252,6 +1259,11 @@ def get_spf_record( timeout_retries=timeout_retries, ) parsed_record["record"] = record + + # Merge warnings from query_spf_record with warnings from parse_spf_record + if query_warnings: + parsed_record["warnings"] = query_warnings + parsed_record.get("warnings", []) + return parsed_record diff --git a/tests.py b/tests.py index 0e0a210..3b31b54 100755 --- a/tests.py +++ b/tests.py @@ -5,6 +5,7 @@ import os import unittest +from unittest.mock import patch import checkdmarc import checkdmarc.bimi @@ -561,6 +562,37 @@ def testSPFBrokenExpModifierMacro(self): domain, ) + def testUndecodableCharactersInNonSPFRecord(self): + """Non-SPF TXT records with undecodable characters should be ignored with a warning""" + domain = "example.com" + + # Mock query_dns to return: + # 1. An undecodable non-SPF TXT record + # 2. A valid SPF record + with patch('checkdmarc.spf.query_dns') as mock_query_dns: + # First call for SPF type records (returns empty) + # Second call for TXT records (returns undecodable + valid SPF) + mock_query_dns.side_effect = [ + [], # No SPF type records + [ + "Undecodable characters", # Non-SPF with undecodable chars + '"v=spf1 include:spf.smtp2go.com -all"' # Valid SPF record + ] + ] + + # This should succeed and return the valid SPF record + result = checkdmarc.spf.get_spf_record(domain) + + # Verify the SPF record was found + self.assertIsNotNone(result["record"]) + self.assertIn("v=spf1", result["record"]) + + # Verify a warning was added for the undecodable record + self.assertTrue(len(result["warnings"]) > 0) + self.assertTrue( + any("undecodable characters" in w.lower() for w in result["warnings"]) + ) + if __name__ == "__main__": unittest.main(verbosity=2) From 34ca8d7f697846de0c03269bd6a3f45393539720 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:36:52 +0000 Subject: [PATCH 3/3] Update warning message to be more accurate about uncertainty Changed warning message from assuming it's a non-SPF record to acknowledging that we cannot determine the record type due to encoding issues. Co-authored-by: seanthegeek <44679+seanthegeek@users.noreply.github.com> --- checkdmarc/spf.py | 4 ++-- tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/checkdmarc/spf.py b/checkdmarc/spf.py index 3ff44ac..e979bad 100644 --- a/checkdmarc/spf.py +++ b/checkdmarc/spf.py @@ -447,9 +447,9 @@ def query_spf_record( # Check for undecodable characters if record == "Undecodable characters": - # We can't determine if this is an SPF record, so treat it as a warning + # We can't determine if this is an SPF record due to encoding issues warnings.append( - "A non-SPF TXT record contains undecodable characters and was ignored." + "A TXT record with undecodable characters was skipped." ) continue diff --git a/tests.py b/tests.py index 3b31b54..c1d6d9a 100755 --- a/tests.py +++ b/tests.py @@ -575,7 +575,7 @@ def testUndecodableCharactersInNonSPFRecord(self): mock_query_dns.side_effect = [ [], # No SPF type records [ - "Undecodable characters", # Non-SPF with undecodable chars + "Undecodable characters", # TXT record with undecodable chars '"v=spf1 include:spf.smtp2go.com -all"' # Valid SPF record ] ] @@ -590,7 +590,7 @@ def testUndecodableCharactersInNonSPFRecord(self): # Verify a warning was added for the undecodable record self.assertTrue(len(result["warnings"]) > 0) self.assertTrue( - any("undecodable characters" in w.lower() for w in result["warnings"]) + any("TXT record" in w and "undecodable" in w.lower() for w in result["warnings"]) )