-
v0.26.0
Stablereleased this
2026-08-20 16:43:15 -05:00 | 34 commits to main since this releaseTwo gaps in
unsubscribe_message, found by reading the code rather than by waiting for
a message to trip them. Both are about the same thing: the tool exists so a POST is not
made blind, and neither the URI it POSTs to nor the answer it gets back was fully
accounted for.Added
-
A redirect now says where it pointed.
redirect_seenreported that the endpoint
answered3xx— non-compliant under RFC 8058, which forbids senders from returning one —
but not the part worth knowing, which is whether the endpoint the sender's DKIM signature
covered just tried to hand this mailbox to a host it did not. TheLocationheader is
read off the response already in hand:redirect_hostnames it, and
redirect_offsiteis true when it differs from the host the POST went to. Both appear
only on a redirect, so their presence is the signal.Still not followed, and that is deliberate. Following would re-POST the subscriber
token to a host nothing vouched for. This reports a destination; it does not visit one,
and it costs no second request. Host only, never theLocation's query string — the
same rule that keeps the unsubscribe URI out of logs and responses, and senders routinely
carry the token through into the redirect.On an off-site redirect the response's
notesays outright that the mismatch is
something to tell the user rather than act on: the POST has already landed, so there
is nothing to retry, and a model reading302without that sentence will report the
unsubscribe as having failed.
Fixed
-
A malformed unsubscribe URI raised instead of refusing.
List-Unsubscribe: <https://[oops>passes the scheme check, andurlsplitdoes not return nothing on an
authority like that — it raisesValueError("Invalid IPv6 URL"), on the first line of
post_one_click, past the gate and outside the(MailError, BadMessageId)the tool
layer catches. A barehttps://with no host got as far as a pointless request. Both are
now the shape refusalunsubscribe_uri_unusable, decided indecidealongside
not_httpsand before anything leaves the machine. Not overridable: there is nowhere to
send.This is the module's own rule applied to a case that escaped it — a refusal is a
result, not an exception, because the caller has to tell the user why nothing happened
and a traceback is not that sentence.
Verified
-
The transport-failure path now has tests that exercise the conversion, not just the
wording it produces. The existing test faked theMailErrorat the client boundary,
which proved the envelope and nothing about theexcept httpx.HTTPErrorthat raises it.
Four failure modes go through it — DNS, connection refused, connect timeout, read
timeout — plus assertions that the error text and the log line carry neither the URI nor
the sender's host, since httpx puts the URL in the message of some transport errors.Guarded by a test asserting the fixture actually reaches the POST. A message that quietly
started failing the gate would leave all four passing while testing nothing. -
unsubscribe_headers_unsignedwas checked and needed no change. It is unconditionally
blocking:allow_unauthenticatedcannot carry it, whether or not a hygiene flag is
present alongside. Signature coverage is a question about the URI's integrity, not the
sender's identity, so the authentication override has no business carrying it — already
the case since 0.24.0, and already pinned by a test.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-