• v0.32.0 35e035fea3

    v0.32.0 — G5: the irreversible surface, and an expunge that does not lie
    All checks were successful
    CI / gate (push) Successful in 1m48s
    CI / image (push) Successful in 19s
    Stable

    joeyr released this 2026-08-22 22:07:15 -05:00 | 2 commits to main since this release

    G5. The Gmail tenant's irreversible surface. expunge_message behind
    MAIL_EXPUNGE_ENABLED, unsubscribe_message behind MAIL_UNSUBSCRIBE_ENABLED.

    The tool list changed, so reconnect the Gmail connector.

    Added

    • expunge_message, and it routes through the Bin because nothing else works.
      expected_subject required by the signature, checked against a real ENVELOPE before
      anything moves, and an untitled message cannot be destroyed through it at all.

      A straight port of the mail tenant's method would have lied. icloudmail flags
      \Deleted and expunges in place; that destroys the message there. Probe 11 measured the
      identical sequence here and it archives. Gmail has a per-account setting — "when a
      message is marked as deleted and expunged from the last visible IMAP folder"
      — whose
      default is Archive, and it is not readable over IMAP. Measured:

      expunge inside result
      a label the message also has others for the LABEL goes, the message does not
      its last label archived into All Mail
      All Mail nothing at all
      the Bin destroyed

      So the Bin route is unconditional — the setting cannot be read, so it is the only
      implementation that means the same thing on every account. This would have shipped the
      exact failure this project exists to refuse, inside the one tool whose whole description
      is a promise of irreversibility. Written up in CLAUDE.md under An expunge does not mean
      the same thing on every server
      .

    • unsubscribe_message, one-click only on this tenant. No url argument, ever — the
      URI comes from the message's own signed header, read server-side, because a tool that
      POSTs to a URL it was handed is an open POST proxy reachable by text sitting in an email.
      A mailto:-only sender is refused with mailto_only: that half sends mail and this
      tenant has no submission path until G4. send_enabled=False is passed to the gate
      hard-coded rather than read from config, so a deployment that turned
      MAIL_SEND_ENABLED on cannot get a gate approving a mailto this client then cannot send.

      The gate logic, the flag allowlist, allow_unauthenticated carrying an authentication
      refusal and nothing else, redirects reported and never followed — all of it is
      mail/unsubscribe.py unchanged since 0.24.0. None of it was re-decided.

    • unsubscribe_message deliberately does NOT nest inside the write gate, unlike
      expunge_message which does. It changes nothing in this mailbox — it sends a request to a
      third party — so the write allowlist has no bearing on whether it is safe, and requiring
      it would tie a decision about talking to strangers to a decision about filing mail.

    Fixed

    • A server that can unsubscribe no longer calls itself READ-ONLY. Because the tool
      registers with no write allowlist at all, the read-only sentence and a tool that
      irreversibly tells a stranger the mailbox is live could coexist — and did, in the first
      draft. Caught by the surface test rather than by review. READ-ONLY is kept for the case
      where it is true, which is also the default deployment.

    Notes

    • The phase-ordering argument was measured and turned out to be about a different
      mailbox.
      docs/gmail-plan.md had unsubscribe shipping after send, because 24 of 40
      iCloud senders offering anything offered only mailto:. On Gmail: 200 INBOX messages
      sampled, 61 offering a facility — 30 both, 27 one-click only, 4 mailto only. 57 of 61
      (93%) usable with no sending capability at all.
      G4 would add almost nothing to this
      tool's reach here.

      The lesson is not "unsubscribe goes early" but that a phase-ordering argument built on
      one account's header census is a fact about that account.
      Re-measuring cost four lines.

    • Every Gmail probe had been failing to clean up for weeks, in exactly the way probe 11
      now documents.
      Each teardown expunged inside its own scratch label and printed
      "removed N"; a sweep found 14 supposedly-destroyed fixtures sitting in All Mail,
      including from probe 8 — the probe whose own finding is that a label expunge does not
      delete. probes/_gmail_cleanup.py is now the single correct implementation and every
      Gmail probe calls it. Gitignored, so nothing here changes.

    • Live pass run: probes/g5_verify.py, 21 checks, all green, including the one that
      matters — after expunge_message returns, the message is gone from every label on the
      real server, All Mail included. Written up in docs/operations.md. Still owed: one
      real one-click unsubscribe, chosen deliberately, because succeeding at that tells a
      stranger this mailbox is live and is not a thing to do for a test.

    Downloads