Sean WhalenandClaude Fable 5.1 316910e1ea fix: never close a caller-supplied file object in parse_report_file/extract_report (#909)
parse_report_file() and extract_report() both closed a file-like object
the caller passed in once they finished reading it (on the success path
only). A function must not close a handle it did not open: the caller
may still want to seek(0) and retry, log tell(), or reuse the handle.

- parse_report_file(): the caller-supplied-object branch no longer calls
  .close(). A path is still opened and closed via `with`; bytes are still
  wrapped in a function-owned BytesIO that the function closes.
- extract_report(): the seekable-stream branch previously aliased the
  caller's stream into `file_object` and then unconditionally closed it
  in `finally`, on the success path and on the exception path alike. An
  `owns_file_object` flag now tracks whether `file_object` is a buffer
  this function created (str/bytes input, or a copy made from a
  non-seekable caller stream) versus an alias of the caller's own
  seekable stream; only the former is closed.
- parse_aggregate_report_file() forwards its input straight to
  extract_report() and does no closing of its own, so it inherits the
  fix; its docstring now says so and points to extract_report()'s for the
  exact post-call position.

Both extract_report()'s and parse_report_file()'s docstrings now state
the contract explicitly. extract_report()'s docstring initially claimed
a successful call leaves a seekable caller stream "positioned at 0" and
described the seek(0) as seeking "back" to the caller's prior position;
neither is true. The unconditional stream.seek(0) after reading the
6-byte header runs regardless of where the caller's stream was
positioned, and the subsequent content read (member/gzip/text) advances
the stream again, so a successful call typically leaves it at EOF, not
0 (measured: XML 866, gzip 428, zip 901/974). The docstring now matches
parse_report_file()'s wording: left open, positioned wherever the
function's own reads left it.

Added testParseReportFileLeavesCallerHandleOpenOnSuccess,
testExtractReportLeavesSeekableCallerStreamOpen (success path), and
testExtractReportLeavesSeekableCallerStreamOpenOnError (exception path,
completing the "never closed on success or failure" claim, which had
gone untested on its exception half) -- asserting real BytesIO `.closed`
state and, for the success cases, that the handle is still readable
after seek(0). All three fail against the pre-fix code; verified with
`parsedmarc.__file__` that the import resolved to this worktree's
package, not a stale venv install, before trusting the failure/pass
results. Updated testParseReportFileLeavesCallerHandleOpenOnReadError's
docstring, which described the old "closed on success" behavior as
intentional.

Also added testParseAggregateReportFileLeavesCallerStreamOpen: the
contract paragraph added to parse_aggregate_report_file()'s docstring
was previously untested through that entry point -- its existing
stream-based tests only exercise extract_report() and parse_report_file()
directly, while parse_aggregate_report_file() itself was only ever
called with bytes. The new test passes a caller-owned BytesIO through
parse_aggregate_report_file() and asserts it is still open after both a
successful parse and an InvalidAggregateReport raised on garbage
content. It fails against the pre-fix code (AssertionError: True is not
false on the success-path assertion); verified via `parsedmarc.__file__`
that the import resolved to this worktree's package before trusting the
result.

CHANGELOG: added an Unreleased/Changes entry (behavior change for
library callers, naming all three affected functions) and corrected the
existing Unreleased/Bug fixes entry for parse_report_file's path-close
fix, whose closing sentence about caller-supplied objects this PR makes
false and whose cross-reference to "the Changes section below" pointed
the wrong direction (Changes is above Bug fixes).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 11:43:46 -04:00
2018-02-05 20:23:07 -05:00
2026-03-09 18:24:16 -04:00

parsedmarc

Build
Status Code
Coverage PyPI
Package PyPI - Downloads

A screenshot of DMARC summary charts in Kibana

parsedmarc is a Python module and CLI utility for parsing DMARC reports. When used with Elasticsearch and Kibana (or Splunk), or with OpenSearch and Grafana, it works as a self-hosted open-source alternative to commercial DMARC report processing services such as Agari Brand Protection, Dmarcian, OnDMARC, ProofPoint Email Fraud Defense, and Valimail.

Note

Domain-based Message Authentication, Reporting, and Conformance (DMARC) is an email authentication protocol.

Sponsors

This project is maintained by one developer. Please consider sponsoring my work if you or your organization benefit from it.

Features

  • Parses aggregate/rua DMARC reports: the legacy draft and 1.0 schemas (RFC 7489) and the new RFC 9990 schema for the final DMARC standard (RFC 9989)
  • Parses failure/ruf DMARC reports (RFC 6591 and RFC 9991; formerly called forensic reports)
  • Parses reports from SMTP TLS Reporting (TLS-RPT, RFC 8460)
  • Can parse reports from an inbox over IMAP, Microsoft Graph, or Gmail API
  • Transparently handles gzip or zip compressed reports
  • Consistent data structures
  • Simple JSON and/or CSV output
  • Optionally email the results
  • Optionally send the results to Elasticsearch, OpenSearch, Splunk, or PostgreSQL, for use with premade dashboards
  • Optionally send the results to Apache Kafka, Amazon S3, Azure Log Analytics (Microsoft Sentinel), a Graylog (GELF) endpoint, a syslog server, or an HTTP webhook

Python Compatibility

This project supports the following Python versions, which are either actively maintained or are the default versions for RHEL or Debian.

Version Supported Reason
< 3.6 End of Life (EOL)
3.6 Used in RHEL 8, but not supported by project dependencies
3.7 End of Life (EOL)
3.8 End of Life (EOL)
3.9 Used in Debian 11 and RHEL 9, but not supported by project dependencies
3.10 Actively maintained
3.11 Actively maintained; supported until June 2028 (Debian 12)
3.12 Actively maintained; supported until May 2035 (RHEL 10)
3.13 Actively maintained; supported until June 2030 (Debian 13)
3.14 Supported (requires imapclient>=3.1.0)
S
Description
No description provided
Readme Apache-2.0
344 MiB
Languages
Python 98.6%
Shell 1.3%