Skip to main content

aprf-reader

Reads APRF (Aggregate Performance Reporting) reports and tells you where the mail you signed landed: how much reached the inbox, how much was treated as unwanted, and how recipients reacted, day by day and per DKIM selector. It points out the days worth a look, such as a jump in unwanted mail.

Two ways to use it, with the same results:

  • In your browser: aprf.dns-audit.com. Drop your reports on the page. They are read on your computer and never uploaded.
  • On the command line: pipx install aprf-reader.

Status: 0.5, beta. Reads reports in the format of draft-ietf-mailmaint-aprf-00.

What APRF is

APRF is a proposed standard for a daily report from a mailbox provider about the mail you signed with DKIM. It is an IETF working group draft, draft-ietf-mailmaint-aprf, adopted by the Mail Maintenance (MAILMAINT) working group in October 2026. Reports arrive by email as small JSON files, one per day per signing domain and selector. Comcast sends them today in a beta for comcast.net mail.

A plain-language introduction: What APRF inbox placement reports tell you, and what they don't.

Use it

Python 3.11 or later, no other dependencies. Install with pipx, which keeps command line tools in their own environment, or with pip (pip3 on some systems) inside a virtual environment.

pipx install aprf-reader
aprf-reader reports/                      # overview in the terminal
aprf-reader APRF.mbox                     # a mail folder exported as mbox
aprf-reader reports/ --markdown           # Markdown with daily tables
aprf-reader reports/ --html summary.html  # and a page you can open or share
aprf-reader reports/ --json               # the summary as data, for scripts

In a terminal you get an overview: for each stream, each count's latest value, its daily range, and a trend line with a gap for each day with no report, then the changes worth a look. Piped or written to a file with --md, the output is Markdown with the full daily tables.

Point it at report files (.json, .json.gz), saved report emails (.eml), mailbox exports (.mbox), .zip files of any of those, or folders of them. The easiest way to collect reports is a mail filter that moves them into one folder, then an export of that folder: Gmail through Google Takeout (pick Mail and the label), Thunderbird with the ImportExportTools NG add-on, or Apple Mail's Mailbox > Export Mailbox. Comcast's reports come from aprf-noreply@alerts.xfinity.com with subjects starting APRF:. Folders are searched recursively. Files it skips are listed on stderr and at the end of the summary, each with a reason.

Each stream (provider, signing domain, selector) gets a table with one row per day and segment, a trend line per count, and a row for each day with no report. A change worth a look is a day when unwanted mail or negative reactions at least doubled and grew by 200 or more, or inbox mail fell by half and by 200 or more, against the report before. Comcast counts in steps of 100, so smaller moves are left out as rounding. A change in the report's version field is called out, since the draft uses it to mark a change in what a category counts.

What it does

  • Read report files (.json, .json.gz, .eml, .mbox and .zip) or a folder of them, and skip duplicates and files it cannot read, with a reason for each.
  • Summarise placement per signing domain and selector: inbox, unwanted and recipient reactions, by day and as a trend.
  • Treat the counts as what the draft says they are: rounded into buckets, so they are a signal, not exact rates.
  • Write a Markdown summary and a single self-contained HTML page you can open or share. Nothing is uploaded anywhere.
  • Follow the draft as it changes. The report format lives in one module (src/aprf_reader/report.py), so a new draft revision is a small update.
  • Check each report against the draft and skip, with the reason, any it cannot trust. One bad report in a file does not cost the rest of the file.

Checking against the draft

Every report is checked against draft-ietf-mailmaint-aprf-00, and every message names the section, so a list of them can go straight into a review of the draft. On the web page, Copy for a draft review copies them as text.

Skipped, because the report cannot be trusted:

Problem Section
Not valid JSON 5
A required header field missing or of the wrong type; times not in epoch seconds 5.2.1
Not one UTC day from 00:00:00 to 23:59:59, or ending before it starts 5.1
Body not a list of segments; a segment not an object 5.2.2
A placement or engagement count that is not a whole number 6, 7
A saved email with no report attached 8

Warnings, where the report is still read:

Warning Section
sdi_used is not N/A, N/F, or the sdi value as published in DNS, or is missing 5.2.1
Attachment name or subject not <yyyymmdd>_<DKIM domain>_<DKIM selector>_<source> 8
Message not multipart/report; attachment not application/json (or application/gzip for .json.gz) 8, 8.1
One attachment holding several reports 8
A placement key other than the four section 6 names, or an engagement key other than positive, neutral and negative 6, 5.2.2
A name or subject that writes / or * some other way, as a file name must 8 is silent
The same day twice from one provider left open, note in 8.1

Comcast's beta sends sdi_used as - N/A - Beta - Not in use and a source of Comcast/Xfinity, so its reports carry the sdi_used warning, and a correct file name for them cannot follow section 8 exactly. A file that repeats a report already read, such as the same report as .json and as .eml, is counted once and summed up in one line.

How it decides

  • The same day twice, from the same provider: it keeps the first copy it reads and skips the other, saying whether the skipped copy had a different version or different counts. The draft leaves resends open (a note in section 8.1 asks whether a later report should overwrite or be discarded), and version is a free string with no order, so there is no defined way to pick the newer one. Files are read in name order, so the first copy is the first by file name, then by position in the file.
  • The same day from two providers: both are kept, as separate streams.
  • Names: signing domains and selectors compare without case, and an international domain matches its xn-- form, so bücher.example and xn--bcher-kva.example are one stream.
  • The reporting window: section 5.1 says a report MUST cover one UTC day, from 00:00:00 to 23:59:59. A report that covers more, or is shifted off the day boundary, is skipped with a reason that names section 5.1.
  • Counts: whole numbers only. 100.0 reads as 100; "100" and negative numbers are refused.

How the web page is built

web/src/aprf.js is a JavaScript port of the Python reader. tests/test_web_parity.py runs both on the same files (JSON, gzip, saved emails, forwarded emails, mailbox exports, duplicates and hostile text) and requires the same JSON, Markdown and HTML output, so the page and the command line cannot drift apart. node web/build.js inlines everything into one web/dist/index.html whose Content Security Policy blocks every network request, so it also works saved to disk. A merge to main that passes the tests deploys it to Cloudflare.

  • dns-audit.com checks a domain for an APRF record and where its reports would go.

License

MIT

Metadata

Release files for aprf-reader 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aprf-reader 0.5.0
File Size Uploaded Provenance
aprf_reader-0.5.0.tar.gz 28.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for aprf-reader 0.5.0
File Interpreter ABI Platform
aprf_reader-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 50.5 kB

Release files / aprf_reader-0.5.0.tar.gz

Download URL aprf_reader-0.5.0.tar.gz
Size 28.9 kB
Tags Source
SHA-256 checksum
How to use checksums
0ec531c86a07d942215a4a2a595465c73f9542e61b91f3e070c6d204b1a53852
BLAKE2b-256 checksum
How to use checksums
cfc59b3894e19be5498cb0db701668e3afb14dc55877038b1462c5b52be04ee3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Release files / aprf_reader-0.5.0-py3-none-any.whl

Download URL aprf_reader-0.5.0-py3-none-any.whl
Size 21.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b9c270fa7cc836bfa9d330a9e28e43e86056ce5283d034a2c3b7785592f8d159
BLAKE2b-256 checksum
How to use checksums
000321a5890e68f587459eca4d8edc92c64f652635442075bd4eed1ef2b431e5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Security for aprf-reader 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for aprf_reader-0.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page