github domainaware/parsedmarc 10.5.0

3 hours ago

New features

  • DNS over HTTPS (DoH) and DNS over TLS (DoT) are now supported through the existing nameservers option (#880). No new configuration option is involved: each entry in the comma-separated list now picks its own transport, and the forms can be mixed in one list. An IP address means plain DNS over UDP and TCP port 53, exactly as before; an https:// URL means DoH; and tls://ip[:port][#hostname] means DoT, where the port defaults to 853 and the optional #hostname supplies the TLS certificate identity (SNI) to verify the server against, matching systemd-resolved's syntax. DoH queries go through a per-process httpx client with its environment trust left at the default, so they honor the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables — the motivating case, a corporate network that blocks outbound DNS but offers an HTTP proxy, where the proxy also resolves the DoH server's own hostname, so no access to UDP port 53 is needed at all — as well as SSL_CERT_FILE/SSL_CERT_DIR, for a private CA in front of a TLS-inspecting proxy. dnspython's own DoH support cannot do this, because it builds its HTTP client with a custom transport, which is precisely the condition under which httpx ignores proxy environment variables. DoT connections are made directly to TCP port 853 and do not use a proxy. A nameservers list of plain IP addresses — including the default — behaves exactly as it did before, and the pre-flight DNS check at startup now exercises whichever transports are configured, so a bad DoH or DoT entry is reported before any mailbox is opened. The dnspython requirement is now dnspython[doh]>=2.7.0.

Bug fixes

  • Failure report MIME parts are now decoded according to their Content-Transfer-Encoding (#882). parse_report_email() read every part's payload without asking the standard library to decode it, so any transfer encoding was left in place. Two things went wrong as a result. A quoted-printable text/rfc822-headers sample part kept its RFC 2045 §6.7 soft line breaks — a = followed by a line ending, with none of the RFC 5322 folding whitespace a header parser needs — which split long headers mid-value; the sample's From header became unparseable, and since the report itself carried no Reported-Domain field, the whole failure report was discarded with TypeError: 'NoneType' object is not subscriptable. Separately, a quoted-printable message/feedback-report part parsed "successfully" with silently corrupted field values, such as an authentication_results of mx.example.com; dmarc=3Dfail. Decoding is applied only to the message/feedback-report part and to the sample part; all other branches (attachments, application/tlsrpt+*, text/plain) still receive the raw payload, since they decode base64 and sniff zip/gzip magic themselves. Parts declaring no transfer encoding, or a 7bit/8bit one, are likewise left alone: there is nothing to undo, and running such a part through the standard library's decoder would round-trip its already-correct text through raw-unicode-escape bytes and replace every non-ASCII character with U+FFFD. Note that a composite part carrying a real transfer encoding is illegal per RFC 2045 §6.4, but reporters send them anyway, and the standard library nests the still-encoded text as a child message rather than decoding it — so those parts are decoded by hand. That nested parse needs one repair first: a soft line break splits a long field onto a continuation line with no colon and no leading whitespace, so the parser treats the remainder as a message body and re-serializing inserts a blank line the encoded text never had, which would otherwise truncate the value at its first split. Any unexpected failure in the new decoding step falls back to the previous behavior, so no report that parsed before can start failing because of it.
  • Feedback report field values no longer keep the carriage return of a CRLF line ending. A message/feedback-report part is a MIME body part, so its lines end with CRLF per RFC 5322 §2.1, but the field regex matched the value with .+ under re.MULTILINE, where $ matches before the LF and not before the CR — so every value ended in a stray \r. The bug was masked because the base64 branch above decoded through str(bytes) and then stripped the resulting literal \r escape sequences; now that parts are properly transfer-decoded, real CRLF reaches the regex, and the regex excludes the line ending explicitly.
  • A failure report with no Reported-Domain field whose sample has no usable From header is now rejected with a clear message instead of crashing with Unexpected error: 'NoneType' object is not subscriptable. reported_domain is a required string in the FailureReport type and is read directly by the Elasticsearch and OpenSearch outputs, so it cannot be defaulted to None; such a report is unusable and is reported as invalid, naming both the missing field and the unparseable From header. As part of this, InvalidFailureReport raised inside parse_failure_report() now propagates unchanged instead of being caught by the function's catch-all handler and re-wrapped, so pre-existing messages such as "Failure sample is not a valid email" are no longer prefixed with Unexpected error:.

Don't miss a new parsedmarc release

NewReleases is sending notifications on new releases.