• v0.25.0 e1fb4127bf

    0.25.0 — folder administration
    All checks were successful
    CI / gate (push) Successful in 1m24s
    CI / image (push) Successful in 14s
    Stable

    joeyr released this 2026-08-20 13:14:15 -05:00 | 36 commits to main since this release

    M4: the mailbox's structure, not the messages in it. create_folder, rename_folder and delete_folder, behind MAIL_FOLDER_ADMIN — off by default, and off means the tools are absent rather than registered and refusing.

    Parallel to MAIL_WRITABLE_FOLDERS rather than nested under it, which is the position CALENDAR_ADMIN already takes: managing a folder is a different question from managing what is inside it, and neither implies the other.

    Where it diverges from the calendar, on purpose

    delete_calendar reaches every collection on the account. rename_folder and delete_folder act only on folders inside MAIL_FOLDERS, because the two allowlists are not the same kind of thing: CALENDARS selects among collections the user picked, while MAIL_FOLDERS is the read boundary for this whole tenant and makes a folder invisible.

    Reaching past it would destroy mail this server was never permitted to read — and worse, the count guard below would answer "how many messages are in the folder you cannot see" for any name a caller cares to try, turning a guard into an oracle. create_folder is necessarily exempt: the name does not exist yet, so there was nothing to have granted.

    delete_folder takes guards, not a force flag

    The plan said force=True. That is not a guard — it is a boolean a model sets while re-reading its own last refusal, and CLAUDE.md had already settled the question: destructive tools make the caller name what they are destroying.

    So it takes expected_name and expected_message_count, the two-step shape delete_calendar established. Calling without the count refuses and reports it, which puts the number in front of a human before the second call acknowledges it. A test pins both arguments into the schema and pins force out of it.

    The messages do not go to trash. They go with the folder — the second operation in this tenant that nothing can undo, after delete_message(permanent=true). INBOX is refused because IMAP forbids it anyway; trash, sent, drafts and junk are refused because this tenant addresses them, and deleting trash destroys what the user throws away in order to be able to undo.

    rename_folder invalidates handles

    Every outstanding message_id naming the old folder stops working, because a handle carries its folder name verbatim. Inherent, not a defect — but the description says so, or the model reuses handles it read moments earlier and reads the resulting "no such folder" as a failure rather than as its own doing.

    Renaming a folder out of MAIL_FOLDERS is refused rather than warned about: this server loses sight of it immediately, including the ability to rename it back.

    Also

    • imap/admin log phase — no folder name logged, only that it happened and, for a delete, how many messages went.
    • folder_admin=on|off on the boot log's grants line, and a preflight check for a gate that is on with nothing it may act on.
    • All three bust the list_folders cache, or the next read serves a stale answer to a model that was just told the call succeeded.

    Not verified

    RENAME has never run against the live account. CREATE and DELETE were exercised during probe 5; RENAME was not, and it is the one operation here carrying a live-verify condition. Unit tests cannot clear it — the fake renames a folder in a dict, and what needs checking is that a stale handle fails cleanly rather than resolving to a different message under a colliding UIDVALIDITY. Probe 10 is written up in docs/icloud-mail-plan.md.

    The same sentence was true of move_message for four releases. Treat this as working-but-unproven.

    969 tests green.

    Downloads