• v0.24.0 1237d386cb

    v0.24.0
    All checks were successful
    CI / gate (push) Successful in 1m25s
    CI / image (push) Successful in 15s
    Stable

    joeyr released this 2026-08-20 00:44:37 -05:00 | 38 commits to main since this release

    The server could see that a sender offers a one-click unsubscribe and could not use
    it.
    get_message has reported unsubscribe: {one_click: true} since 0.19.0, so the
    answer to "unsubscribe me from this" was "tap the button in Mail" — which is one tap, and
    fine. The reason to build the tool anyway is the gate, not the convenience: hitting an
    unsubscribe endpoint on a hostile message confirms to the sender that the mailbox is live
    and read. A person tapping Unsubscribe cannot tell whether the sender authenticated. This
    server already computes that, so it can decline to be the thing that answers a phish.

    Added

    • unsubscribe_message, performing the RFC 8058 one-click POST for one message,
      behind MAIL_UNSUBSCRIBE_ENABLED. Off by default, and off means the tool is not
      registered rather than registered and refusing — the rule MAIL_WRITABLE_FOLDERS
      already follows, and it matters more here, because the fallback when the tool does not
      exist is one tap in Mail.

      It takes a message_id and has no URL parameter. The URI is read server-side from
      the message's own DKIM-signed header. A tool that POSTs to a URL it is handed is an open
      POST proxy reachable by anything that can talk to this server, including text sitting in
      an email — so the door is closed by there being no parameter to push on, and a test
      pins the argument schema.

      The request follows the RFC's constraints rather than a generic POST: no cookies, no
      authorization, no redirects followed (senders MUST NOT return one, and chasing it would
      re-POST this mailbox's token to a host the signature never covered), multipart/form-data
      per the SHOULD, and exactly one attempt — a retry against a stranger's endpoint is a
      second unsubscribe request, not another go at the first.

    • A gate scored against the live account before it was written, which is the only
      reason it is the right shape. Probe 9 (2026-08-20, 320 messages across every folder, 16
      one-click) ran the designed rules over real mail and refused 11 of the 16, including
      the clean, DKIM-aligned, p=quarantine sender the design named as the case that must
      proceed. Two conditions were wrong:

      • "no DKIM verdict may be unaligned" refuses ordinary bulk mail. An ESP signs with
        its own d= alongside the sender's, so a correctly aligned message carries an
        unaligned verdict too. The property actually wanted is "the From domain is
        authenticated", which is dmarc=pass — already parsed, already trust-keyed, and true
        via SPF alignment even where no DKIM signature aligns.

      • "any hygiene flag refuses" refuses newsletters. reply_to_elsewhere is what
        "send from no-reply, reply to support" produces and recipient_in_unsubscribe is what
        every ESP does by construction. Together they refused a Constant Contact newsletter
        published under p=reject. Both are now warnings; every other flag blocks.

      The flags that survive as blocking are the ones describing deception, and they are
      what caught the phish. p=none warns and never refuses — 11 of the 16 live one-click
      senders publish it, including every obviously legitimate one.

    • A DKIM h= coverage check on the offer itself. RFC 8058 requires both unsubscribe
      headers to be covered by a valid signature, so coverage proves the URI was not injected
      in transit. Read only off a signature whose d= matches a header.d that a trusted
      verdict recorded as passing — reading h= off an arbitrary signature proves nothing.
      Three states, not two: covered, not covered, and no verified signature to read, with the
      last warning rather than refusing. Four of sixteen live messages carry two verified
      signatures; they agreed in every case, so any one covering both is enough and no
      tiebreak rule exists to be disagreed with later.

      What it does not buy, stated because the temptation is real: the phish signs its own
      mail correctly and its unsubscribe header is validly signed too. Integrity, not good
      faith.

    • allow_unauthenticated, scoped to authentication refusals only. It carries a
      dmarc=fail — four of the sixteen live messages are senders that fail DMARC while
      passing SPF, and a user may well recognise them — and it can never carry a hygiene flag
      or an unsigned offer. "Unauthenticated but I know who they are" is a judgement a person
      can make; "this message is impersonating someone" is not, and the argument is
      model-settable, so the text arguing for it could come from the message being
      unsubscribed from.

    • mail/unsubscribe as a log phase. Its own, for the reason imap/write is separate
      from imap/fetch: this is the only request this server makes to a host it did not
      choose, and "did this server contact that sender" must be answerable without reading
      every mail line. The URI never appears — only its hostname. The query string is a bearer
      token identifying this mailbox, which makes it account data by the rule in client.py,
      and it stays out of tool responses for the same reason.

    Fixed

    • A List-Unsubscribe header that is present and empty is no longer reported as
      unreadable.
      Eleven messages in the live INBOX arrive that way — confirmed against a
      full BODY.PEEK[] read, so it is the senders and not the fetch. unparsed: true reads
      to a model as "there is an unsubscribe here I could not read", which has it tell the
      user a facility might exist. The sender advertised nothing, so the true sentence is the
      one absence produces. The three states are unchanged and the middle one still means what
      it says: content that defeated the parser, not the absence of content.

    Not built, deliberately

    • No bulk mode. One message per call. A loop over a folder announces the mailbox as
      live to every sender in the batch, which is the exact outcome the gate exists to avoid.
    • No mailto: unsubscribe. This server cannot send mail — that is M3. It is not a
      small gap: 24 of the 40 live senders offering any facility offer only a mailto, more
      than the 16 this tool covers. Reported by name rather than failing opaquely.
    • No auto-move to junk afterwards. Junking is a separate call, and this stays atomic.
    Downloads