Skip to content

Make, rename and throw away a folder - #21

Merged
rutgerhofste merged 1 commit into
mainfrom
claude/optimistic-carson-ljrqh3
Sep 16, 2026
Merged

rutgerhofste merged 1 commit into
mainfrom
claude/optimistic-carson-ljrqh3

Conversation

@rutgerhofste

Copy link
Copy Markdown
Member

Closes #17.

mail_list_folders read folders and mail_move filed into them, but a folder itself could only be made in another mail client first. Three tools close that, on imap-tools' own folder commands:

Tool Gate
mail_create_folder(name, parent=None) none -- a new folder holds nothing
mail_rename_folder(name, new_name) none -- reversible, and the mail travels with it
mail_delete_folder(name, confirm) confirm=true, and the folder must be empty

Two decisions

A path is not the caller's to type. The delimiter is / on one server and . on the next, and a mailbox that keeps everything under INBOX refuses a bare top-level name. So create takes a parent folder and the backend joins it with the delimiter it read off LIST -- the same not-knowable rule the SPECIAL-USE roles are written with. A rename given a bare name keeps the folder where it is, for the same reason. Creating a folder that is already there is not a failure: created=False.

Delete is the one genuinely irreversible thing in this package. IMAP's DELETE takes the folder's messages with it and no Trash catches them, which is exactly what mail_delete was written not to do. So it refuses a folder that still holds mail or sub-folders, with the count -- empty it the recoverable way first (mail_delete files the messages in Trash) and the folder then goes. The confirm gate answers "did the user ask for this"; the refusal answers "can they get it back", so it has both. INBOX and the five special-use folders are refused outright, by their flags or by special_folder's fallback name, since a server advertising no SPECIAL-USE still has the Trash this package files deletes into.

Changes

  • providers/protocol.py -- three methods on MailProvider, read in the tool layer through getattr so a backend without them (the admin package's Graph provider) is refused by name rather than crashed into.
  • providers/soverin/imap.py -- create_folder / rename_folder / delete_folder, plus the delimiter lookup, the load-bearing-folder refusal and the emptiness check (STATUS, falling back to counting uids).
  • tools/mail/folders.py, schemas.py, knowledge.py, README.md, CLAUDE.md.
  • tests/test_folder_admin.py (34 tests) and four GreenMail e2e cases that make, fill, rename and delete a folder against a real server.

Testing

make lint and make test (316 passed) are green. make test-int was not run: there is no Docker in this session, so the four new GreenMail cases are unverified against a real server -- CI covers them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W9TGZFHaq8x4vPvQ6kSQX6


Generated by Claude Code

mail_list_folders read folders and mail_move filed into them, but a folder
itself could only be made in another mail client first -- so "put these in a
folder called Belastingdienst" was a reasonable ask this server could not
answer. mail_create_folder / mail_rename_folder / mail_delete_folder close
that, on imap-tools' own folder commands.

Two decisions shape them.

A path is not the caller's to type. The hierarchy delimiter is "/" on one
server and "." on the next, and a mailbox that keeps everything under INBOX
refuses a bare top-level name -- so create takes a parent *folder* and the
backend joins it with the delimiter it read off LIST, the same not-knowable
rule the SPECIAL-USE roles are written with. A rename given a bare name keeps
the folder where it is, for the same reason. Creating a folder that is already
there is not a failure; it comes back created=False.

And delete is the one genuinely irreversible thing in this package: IMAP's
DELETE takes the folder's messages with it and no Trash catches them, which is
exactly what mail_delete was written not to do. So it refuses a folder that
still holds mail or sub-folders and says how many -- empty it the recoverable
way first and the folder then goes. The confirm gate answers "did the user ask
for this"; the refusal answers "can they get it back", so it has both. INBOX
and the five special-use folders are refused outright rather than gated, by
their flags or by special_folder's fallback name, since a server advertising no
SPECIAL-USE still has the Trash this package files deletes into.

tests/test_folder_admin.py pins the joining, the refusals and the emptiness
check; the GreenMail e2e makes, fills, renames and deletes one for real.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W9TGZFHaq8x4vPvQ6kSQX6
@rutgerhofste
rutgerhofste marked this pull request as ready for review September 16, 2026 08:59
@rutgerhofste
rutgerhofste merged commit c3514fe into main Sep 16, 2026
3 checks passed
@rutgerhofste
rutgerhofste deleted the claude/optimistic-carson-ljrqh3 branch September 16, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to create, rename or delete a folder

1 participant