Junk Mail can be emptied in one action, from three places: a banner across the top of the folder, the folder's right-click menu, and the list's own menu.
Junk Mail 1
┌──────────────────────────────────────────────────────┐
│ Deleting spam is permanent — it does not go to │
│ Deleted Items first. Delete all spam now │
└──────────────────────────────────────────────────────┘
The messages are destroyed, not moved to Deleted Items — routing spam through the bin leaves you with the same problem in another folder, and "delete all spam" means gone everywhere else. The dialog says so before you commit, and there's no undo.
emptyMailbox already did the hard part: walking a folder a page at a time so it survives maxObjectsInSet, which a Deleted Items of 5192 once didn't. All that changed is which folders it accepts. The guard stays in the store, not only in the menus, so a future caller can't empty the Inbox by asking nicely.
The three entry points share one helper. Three dialogs warning about a permanent deletion in three slightly different ways is how one of them ends up not warning at all. An empty folder shows the item greyed out rather than hidden, so it's where you expect it next time.
Folder naming — two bugs, one cause
This is in the same commit because it touches the same file and because testing the above is what surfaced it.
The mock called its folders Trash and Sent. Stalwart's defaults follow the Exchange convention — Deleted Items, Sent Items. So anything built from a folder's name read differently against the mock than against a real server, and every screenshot in the README shows a folder list no user has.
Worse, in shipping code: the undo toast preferred a hardcoded label over the folder's actual name.
-const name = opts.label ?? mailboxes[toMailboxId]?.name ?? "folder";
+const name = mailboxes[toMailboxId]?.name ?? opts.label ?? "folder";
So on a real server, deleting mail announced "moved to Trash" when the folder is Deleted Items, and reporting spam said "moved to Spam" when it's Junk Mail. The one message whose entire job is saying where mail went was naming somewhere that doesn't exist.
Verified against the mock, by hand
check
result
banner shows in Junk with mail in it
yes
banner hidden once the folder is empty
yes
dialog counts and pluralises
"All 1 message will be deleted permanently"
messages destroyed, not moved
Junk → 0 rows, Deleted Items still empty
right-click menu on Junk
"Delete all spam", danger-styled, greyed out when empty
right-click menu on Archive
offers neither
right-click menu on Trash
still "Empty Deleted Items" — no regression
Plus 249 web + 75 server tests, typecheck and build clean. The store test that used Junk as its example of a disallowed folder now uses Archive, and a new test asserts Junk empties via destroy with no update call anywhere — i.e. nothing was moved.
## Delete all spam
Junk Mail can be emptied in one action, from three places: a banner across the top of the folder, the folder's right-click menu, and the list's own menu.
```
Junk Mail 1
┌──────────────────────────────────────────────────────┐
│ Deleting spam is permanent — it does not go to │
│ Deleted Items first. Delete all spam now │
└──────────────────────────────────────────────────────┘
```
The messages are **destroyed**, not moved to Deleted Items — routing spam through the bin leaves you with the same problem in another folder, and "delete all spam" means gone everywhere else. The dialog says so before you commit, and there's no undo.
`emptyMailbox` already did the hard part: walking a folder a page at a time so it survives `maxObjectsInSet`, which a Deleted Items of 5192 once didn't. All that changed is which folders it accepts. **The guard stays in the store**, not only in the menus, so a future caller can't empty the Inbox by asking nicely.
The three entry points share one helper. Three dialogs warning about a permanent deletion in three slightly different ways is how one of them ends up not warning at all. An empty folder shows the item greyed out rather than hidden, so it's where you expect it next time.
## Folder naming — two bugs, one cause
This is in the same commit because it touches the same file and because testing the above is what surfaced it.
**The mock called its folders `Trash` and `Sent`.** Stalwart's defaults follow the Exchange convention — `Deleted Items`, `Sent Items`. So anything built from a folder's name read differently against the mock than against a real server, and every screenshot in the README shows a folder list no user has.
**Worse, in shipping code:** the undo toast preferred a hardcoded label over the folder's actual name.
```diff
-const name = opts.label ?? mailboxes[toMailboxId]?.name ?? "folder";
+const name = mailboxes[toMailboxId]?.name ?? opts.label ?? "folder";
```
So on a real server, deleting mail announced *"moved to Trash"* when the folder is *Deleted Items*, and reporting spam said *"moved to Spam"* when it's *Junk Mail*. The one message whose entire job is saying where mail went was naming somewhere that doesn't exist.
## Verified against the mock, by hand
| check | result |
|---|---|
| banner shows in Junk with mail in it | yes |
| banner hidden once the folder is empty | yes |
| dialog counts and pluralises | "All **1 message** will be deleted permanently" |
| messages destroyed, not moved | Junk → 0 rows, **Deleted Items still empty** |
| right-click menu on Junk | "Delete all spam", danger-styled, greyed out when empty |
| right-click menu on Archive | offers neither |
| right-click menu on Trash | still "Empty Deleted Items" — no regression |
Plus 249 web + 75 server tests, typecheck and build clean. The store test that used Junk as its example of a *disallowed* folder now uses Archive, and a new test asserts Junk empties via `destroy` with no `update` call anywhere — i.e. nothing was moved.
**Merged** 2026-08-26 as coffey-labs/ihasmail@e45c43100f68
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete all spam
Junk Mail can be emptied in one action, from three places: a banner across the top of the folder, the folder's right-click menu, and the list's own menu.
The messages are destroyed, not moved to Deleted Items — routing spam through the bin leaves you with the same problem in another folder, and "delete all spam" means gone everywhere else. The dialog says so before you commit, and there's no undo.
emptyMailboxalready did the hard part: walking a folder a page at a time so it survivesmaxObjectsInSet, which a Deleted Items of 5192 once didn't. All that changed is which folders it accepts. The guard stays in the store, not only in the menus, so a future caller can't empty the Inbox by asking nicely.The three entry points share one helper. Three dialogs warning about a permanent deletion in three slightly different ways is how one of them ends up not warning at all. An empty folder shows the item greyed out rather than hidden, so it's where you expect it next time.
Folder naming — two bugs, one cause
This is in the same commit because it touches the same file and because testing the above is what surfaced it.
The mock called its folders
TrashandSent. Stalwart's defaults follow the Exchange convention —Deleted Items,Sent Items. So anything built from a folder's name read differently against the mock than against a real server, and every screenshot in the README shows a folder list no user has.Worse, in shipping code: the undo toast preferred a hardcoded label over the folder's actual name.
So on a real server, deleting mail announced "moved to Trash" when the folder is Deleted Items, and reporting spam said "moved to Spam" when it's Junk Mail. The one message whose entire job is saying where mail went was naming somewhere that doesn't exist.
Verified against the mock, by hand
Plus 249 web + 75 server tests, typecheck and build clean. The store test that used Junk as its example of a disallowed folder now uses Archive, and a new test asserts Junk empties via
destroywith noupdatecall anywhere — i.e. nothing was moved.Merged 2026-08-26 as coffey-labs/ihasmail@e45c43100f
Rebuilt from: git history, session transcript.