Nobody is on 0.15, and carrying it was costing more than it returned. The last release that runs on it is tagged stalwart-0.15-support.
Net −721 lines.
Why, precisely
Not the branch count — the failure mode. A wrong answer about which generation had answered always had somewhere to fall back to, so it failed quietly rather than loudly. One capability looked for in the wrong place downgraded every real 0.16 server onto the 0.15 path: it posted the current password to an endpoint 0.16 had removed, reported the wrong generation on About, and ran Files on the older code. It reached production and was recorded here as verified when it wasn't. The mock mirrored the same wrong placement, which is why the tests agreed with it.
With one supported generation, a wrong guess is a loud error on the first call. There is no path left for it to be wrong into.
Removed
web/src/lib/filenode.ts — was 92 lines of shim, now 27 lines of plain constructors
server/src/account.ts — the Backend = "registry" | "legacy" split, the parallel REST implementation of every credential operation, detectBackend/probeBackend/forgetBackend, and assertCurrentPassword/readableSecret which existed only for it (−266)
AccountInfo.generation and everything reading it, including the About row
The mock's LEGACY mode, mock:legacy, dev:mock:legacy
Three test files that existed only to pin 0.15 behaviour: account-legacy.test.ts, filenode.test.ts, and the readableSecret test
The sign-in guard
Refuses an older server once, by name, instead of letting Files, the account locale and credentials each fail separately with nothing connecting them. It says the credentials were fine — someone hitting this has typed a correct password, and telling them otherwise sends them round in circles — and names the tag to build from.
Four tests in login-guard.test.ts cover it, including that no session cookie is minted and that bad credentials on such a server are still a plain 401 rather than the server error. Testing it needed a MOCK_NO_REGISTRY=1 fixture that only omits the capability — the rest of the mock still behaves like 0.16.
hasStalwartRegistry now decides whether anyone can sign in at all, so its tests got kept and re-pointed rather than trimmed: getting it wrong would lock every user out of a working server.
Two judgement calls worth flagging
Both are fallbacks that weren't strictly about 0.15, removed for the same reason as everything else:
Files no longer answers a refused filter or sort by fetching every node in the account. That path only existed because 0.15 refused parentId/isTopLevel. On 0.16 it would hide a real fault behind a performance cliff nobody would notice. It now surfaces the error. If you'd rather keep a fallback here, say so — I'd suggest retrying without the sort rather than abandoning filters wholesale.
ensureFolder/findInFolder filter on parentId/isTopLevel alone and match names client-side. name is not a filter Stalwart is known to implement, and one it does not know fails the whole query rather than being ignored — so this is more robust than what it replaces, not less.
Testing
226 web + 75 server tests pass; typecheck and production build clean. Smoke-tested the built server against the mock end to end: sign-in, the Files tree on the 0.16 path with the app folder correctly hidden, and /api/account/security returning the new shape over the registry.
Not yet exercised against the live 0.16.19. The sign-in path and self-service credentials are the two I'd want eyes on there before this goes to production.
Nobody is on 0.15, and carrying it was costing more than it returned. The last release that runs on it is tagged [`stalwart-0.15-support`](https://github.com/LINUXexpert-org/ihasmail/releases/tag/stalwart-0.15-support).
**Net −721 lines.**
## Why, precisely
Not the branch count — the *failure mode*. A wrong answer about which generation had answered always had somewhere to fall back to, so it failed quietly rather than loudly. One capability looked for in the wrong place downgraded every real 0.16 server onto the 0.15 path: it posted the current password to an endpoint 0.16 had removed, reported the wrong generation on About, and ran Files on the older code. It reached production and was recorded here as verified when it wasn't. The mock mirrored the same wrong placement, which is why the tests agreed with it.
With one supported generation, a wrong guess is a loud error on the first call. There is no path left for it to be wrong *into*.
## Removed
- `web/src/lib/filenode.ts` — was 92 lines of shim, now 27 lines of plain constructors
- `server/src/account.ts` — the `Backend = "registry" | "legacy"` split, the parallel REST implementation of every credential operation, `detectBackend`/`probeBackend`/`forgetBackend`, and `assertCurrentPassword`/`readableSecret` which existed only for it (−266)
- `AccountInfo.generation` and everything reading it, including the About row
- The mock's `LEGACY` mode, `mock:legacy`, `dev:mock:legacy`
- Three test files that existed only to pin 0.15 behaviour: `account-legacy.test.ts`, `filenode.test.ts`, and the `readableSecret` test
## The sign-in guard
Refuses an older server once, by name, instead of letting Files, the account locale and credentials each fail separately with nothing connecting them. It says the credentials were fine — someone hitting this has typed a correct password, and telling them otherwise sends them round in circles — and names the tag to build from.
Four tests in `login-guard.test.ts` cover it, including that **no session cookie is minted** and that bad credentials on such a server are still a plain 401 rather than the server error. Testing it needed a `MOCK_NO_REGISTRY=1` fixture that only omits the capability — the rest of the mock still behaves like 0.16.
`hasStalwartRegistry` now decides whether anyone can sign in at all, so its tests got kept and re-pointed rather than trimmed: getting it wrong would lock every user out of a working server.
## Two judgement calls worth flagging
Both are fallbacks that weren't *strictly* about 0.15, removed for the same reason as everything else:
1. **Files no longer answers a refused filter or sort by fetching every node in the account.** That path only existed because 0.15 refused `parentId`/`isTopLevel`. On 0.16 it would hide a real fault behind a performance cliff nobody would notice. It now surfaces the error. If you'd rather keep a fallback here, say so — I'd suggest retrying without the *sort* rather than abandoning filters wholesale.
2. **`ensureFolder`/`findInFolder` filter on `parentId`/`isTopLevel` alone** and match names client-side. `name` is not a filter Stalwart is known to implement, and one it does not know fails the whole query rather than being ignored — so this is more robust than what it replaces, not less.
## Testing
226 web + 75 server tests pass; typecheck and production build clean. Smoke-tested the built server against the mock end to end: sign-in, the Files tree on the 0.16 path with the app folder correctly hidden, and `/api/account/security` returning the new shape over the registry.
**Not yet exercised against the live 0.16.19.** The sign-in path and self-service credentials are the two I'd want eyes on there before this goes to production.
**Merged** 2026-08-26 as coffey-labs/ihasmail@2a741f640718
<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.
Nobody is on 0.15, and carrying it was costing more than it returned. The last release that runs on it is tagged
stalwart-0.15-support.Net −721 lines.
Why, precisely
Not the branch count — the failure mode. A wrong answer about which generation had answered always had somewhere to fall back to, so it failed quietly rather than loudly. One capability looked for in the wrong place downgraded every real 0.16 server onto the 0.15 path: it posted the current password to an endpoint 0.16 had removed, reported the wrong generation on About, and ran Files on the older code. It reached production and was recorded here as verified when it wasn't. The mock mirrored the same wrong placement, which is why the tests agreed with it.
With one supported generation, a wrong guess is a loud error on the first call. There is no path left for it to be wrong into.
Removed
web/src/lib/filenode.ts— was 92 lines of shim, now 27 lines of plain constructorsserver/src/account.ts— theBackend = "registry" | "legacy"split, the parallel REST implementation of every credential operation,detectBackend/probeBackend/forgetBackend, andassertCurrentPassword/readableSecretwhich existed only for it (−266)AccountInfo.generationand everything reading it, including the About rowLEGACYmode,mock:legacy,dev:mock:legacyaccount-legacy.test.ts,filenode.test.ts, and thereadableSecrettestThe sign-in guard
Refuses an older server once, by name, instead of letting Files, the account locale and credentials each fail separately with nothing connecting them. It says the credentials were fine — someone hitting this has typed a correct password, and telling them otherwise sends them round in circles — and names the tag to build from.
Four tests in
login-guard.test.tscover it, including that no session cookie is minted and that bad credentials on such a server are still a plain 401 rather than the server error. Testing it needed aMOCK_NO_REGISTRY=1fixture that only omits the capability — the rest of the mock still behaves like 0.16.hasStalwartRegistrynow decides whether anyone can sign in at all, so its tests got kept and re-pointed rather than trimmed: getting it wrong would lock every user out of a working server.Two judgement calls worth flagging
Both are fallbacks that weren't strictly about 0.15, removed for the same reason as everything else:
parentId/isTopLevel. On 0.16 it would hide a real fault behind a performance cliff nobody would notice. It now surfaces the error. If you'd rather keep a fallback here, say so — I'd suggest retrying without the sort rather than abandoning filters wholesale.ensureFolder/findInFolderfilter onparentId/isTopLevelalone and match names client-side.nameis not a filter Stalwart is known to implement, and one it does not know fails the whole query rather than being ignored — so this is more robust than what it replaces, not less.Testing
226 web + 75 server tests pass; typecheck and production build clean. Smoke-tested the built server against the mock end to end: sign-in, the Files tree on the 0.16 path with the app folder correctly hidden, and
/api/account/securityreturning the new shape over the registry.Not yet exercised against the live 0.16.19. The sign-in path and self-service credentials are the two I'd want eyes on there before this goes to production.
Merged 2026-08-26 as coffey-labs/ihasmail@2a741f6407
Rebuilt from: git history, session transcript.