Skip to content

Conversation

@aharpour
Copy link
Contributor

Description

This PR improves the handling of SPF macros within checkdmarc. Previously, the parser attempted to resolve DNS records for mechanisms containing macros (e.g., a:%{d}), which would fail or produce incorrect results since macros require runtime context (like the sender's IP or domain) to be expanded.

This update ensures that when a macro is detected in a mechanism or modifier, checkdmarc validates the macro's syntax but skips the associated DNS lookup.

Changes

  • checkdmarc/spf.py:

    • Added logic to detect macros (presence of %) in a, mx, include, ptr, exists mechanisms and redirect, exp modifiers.
    • Implemented syntax validation for these macros using _validate_spf_macros.
    • Prevented DNS lookups for a, mx, include, redirect, and exp when macros are present, treating them as valid but unresolvable in a static context.
    • Ensured these mechanisms are still included in the parsed output.
  • tests.py:

    • Added a comprehensive set of unit tests covering both valid and invalid macro syntax for all supported mechanisms:
      • a: testSPFValidAMechanismMacro, testSPFBrokenAMechanismMacro
      • mx: testSPFValidMXMechanismMacro, testSPFBrokenMXMechanismMacro
      • ptr: testSPFValidPTRMechanismMacro, testSPFBrokenPTRMechanismMacro
      • include: testSPFValidIncludeMechanismMacro, testSPFBrokenIncludeMechanismMacro
      • exists: testSPFValidExistsMechanismMacro, testSPFBrokenExistsMechanismMacro
      • redirect: testSPFValidRedirectModifierMacro, testSPFBrokenRedirectModifierMacro
      • exp: testSPFValidExpModifierMacro, testSPFBrokenExpModifierMacro

Motivation

SPF macros (RFC 7208) allow for dynamic policy records. A static analyzer like checkdmarc cannot resolve these dynamic values. Attempting to do so leads to NXDOMAIN errors or meaningless queries (e.g., looking up %{i}.example.com). This change aligns the parser's behavior with the limitations of static analysis by validating the syntax without performing the impossible resolution.

…t and stop processing them further, since macros can only be validated in the context of an actual email.
@aharpour
Copy link
Contributor Author

Hi Sean,

It turns out the previous pull request had a small bug: it was not incrementing the DNS lookup counts when macros were used. I’ve fixed that issue and created a new pull request. I also added assertions for DNS lookup counts in the unit tests to prevent regressions.

@aharpour
Copy link
Contributor Author

Hi Sean,

I have done a full regression test and detected no regression.

@seanthegeek
Copy link
Contributor

Hi. I'm working on adding detailed typing across the library (I'm not done yet), so there are now mere conflicts. Can you address these so I can merge this?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances SPF record parsing in checkdmarc to correctly handle SPF macros (e.g., %{d}, %{i}). Previously, the parser attempted DNS lookups for mechanism values containing macros, which would fail since macros require runtime context to expand. The changes validate macro syntax while skipping DNS lookups, treating macro-containing mechanisms as statically unresolvable but syntactically valid.

Key changes:

  • Added macro detection (% in value) for a, mx, include, ptr, exists mechanisms and redirect, exp modifiers
  • Implemented syntax validation for all macros using existing _validate_spf_macros function
  • Fixed critical bugs in exp modifier: corrected parentheses placement (len(exp_txt_records) == 0 instead of len(exp_txt_records == 0)) and added missing f-string prefixes

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
checkdmarc/spf.py Added macro detection and DNS lookup skipping for mechanisms/modifiers; fixed bugs in exp modifier (parentheses and f-strings); mechanisms with macros still count toward DNS lookup limits per RFC 7208
tests.py Added comprehensive test coverage for valid and invalid macro syntax across all mechanisms (a, mx, ptr, include, exists) and modifiers (redirect, exp); added DNS lookup count assertions to existing tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@seanthegeek seanthegeek merged commit 4666db0 into domainaware:main Dec 19, 2025
1 check passed
seanthegeek added a commit that referenced this pull request Dec 19, 2025
seanthegeek added a commit that referenced this pull request Dec 20, 2025
* Initial plan

* Convert dict() and OrderedDict() to literal dictionaries with TypedDict annotations in spf.py

Co-authored-by: seanthegeek <44679+seanthegeek@users.noreply.github.com>

* Fix parameter type annotation for 'seen' parameter in parse_spf_record

Co-authored-by: seanthegeek <44679+seanthegeek@users.noreply.github.com>

* Add TypedDict definitions and type annotations to bimi.py

Co-authored-by: seanthegeek <44679+seanthegeek@users.noreply.github.com>

* Properly Reintigrate the code from PR #218

* Code formatting

* Use dict[str, Any]  for BIMI return types

There isn't a way to make a TypeDict field optional until Python 3.11, and we need to support 3.9. Leaving the typedicts in place for now, so that they can be used when Python 3.10 is no longer suported.

* Always raise a warning when a PTR mechanism is used

* Refine warnings in SPF record parsing and update SVGMetadata TypedDict comment for clarity

* Fix macro validation

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
seanthegeek added a commit that referenced this pull request Dec 20, 2025
### Improvements

- Proper validation of SPF macros (PR #218)
- More detailed API typing

### Fixes

- Missing values in some SPF warnings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants