• v0.17.0 5a1cd9bab4

    v0.17.0 — move_message actually moves, and M2 is verified live
    All checks were successful
    CI / gate (push) Successful in 1m18s
    CI / image (push) Successful in 50s
    Stable

    joeyr released this 2026-08-12 15:58:48 -05:00 | 51 commits to main since this release

    move_message never worked, and delete_message without permanent never worked
    either, because it is a move.
    Both failed closed with "the server did not say where it
    landed" for the whole life of M2. Fixed, and M2 is now verified against the live mailbox
    — 27 checks, all passing.

    Fixed

    • The COPYUID was read from the wrong place. IMAPClient.copy() returns None on
      iCloud; imaplib files the response code under untagged_responses["COPYUID"] as
      [b"<validity> <src> <dst>"], already stripped of the keyword. The parser was reading the
      return value, found nothing, and — correctly — refused to expunge rather than guess a
      handle. So the damage was a move that always failed rather than one that lost mail, which
      is the direction that branch was written to fail in.

    • A stale COPYUID can no longer be mistaken for this copy's answer.
      untagged_responses accumulates across commands on one connection, so the key is now
      popped before the COPY as well as after. Left unguarded, a copy that produced no
      response code could read the previous one's and mint a handle naming a different message —
      worse than refusing, and silent.

    Changed

    • The test fake now models where the value actually lives. It previously returned a
      COPYUID string from copy(), which no server does — that is precisely why the suite was
      green while every real move failed. Two regression tests pin the real shape: one that the
      handle comes from untagged_responses, one that a leftover entry is not read as this
      copy's answer.

    Notes

    • This is what the live checklist is for. 799 tests passed against a fake built on the
      same wrong assumption as the code, which is the failure mode this project already had a
      name for: the fakes agree with the code because the same person wrote both. The probe that
      caught it drives the real tool layer against the real mailbox and lives in probes/,
      gitignored, because it names real folders.

    • What the run established, beyond the fix: a moved message's new handle resolves to the
      same message and reports the destination folder; the old handle refuses rather than
      answering; no duplicate is left behind; an omitted update_message argument leaves that
      flag alone; both permanent-delete refusals leave the message intact; a default delete
      lands in the trash folder and can be moved back out; and — the one that is silent and
      unrecoverable when wrong — UID EXPUNGE removed only the message it named, while a
      neighbour flagged \Deleted by another client survived.

    • Still owed for M2: the Trash default confirmed on a phone. The probe checks that the
      handle names the configured trash folder, which is not the same as Apple showing the
      message in the Trash a phone displays.

    Downloads