Newest-first was the only order, so the mail you had not read yet was wherever it happened to fall.
What it does
Seven presets — newest, oldest, unread first, starred first, largest, by sender, by subject — or up to three levels of your own.
It covers the Inbox alone by default: unread-first is what people want in the folder they triage and confusing in Sent, where everything is read and the order that matters is when it went. Widen it to every folder if you want. Search keeps newest-first whatever the setting says, since a result list is already ordered by the question that was asked.
The server does the sorting, over the whole folder — same reason search runs there. A list sorted in the browser is sorted only as far as the browser has loaded, which on a folder of ten thousand is the first fifty and a lie about the rest.
Two things easy to get backwards, both pinned in tests
hasKeyword sorts a boolean and false comes before true. So "unread first" is $seen ASCENDING while "starred first" is $flagged DESCENDING — the other way round. Getting either wrong puts exactly the mail you were looking for at the bottom.
Every order ends with newest-first as a tiebreak. A sort whose last level is a keyword or a subject leaves every tie undefined, and an undefined order changes between two looks at the same folder for no reason the reader can see. (No tiebreak is appended when the sort already ends on receivedAt — a contradictory second one would say nothing.)
The failure this could have caused
Sorting on a keyword is optional in RFC 8621, and a server that will not do it fails the whole query rather than degrading it. So this setting could have turned the Inbox into a folder that does not open.
The refusal is caught once, the keyword levels dropped, the query retried — and nothing is said. The reader asked for an order and got the closest the server can give; a toast on every folder change would be the app complaining about its own request.
The mock was lying
It ignored sort entirely and always answered newest-first — reproducing a server that silently returns a different order from the one asked for, which is the one shape of wrongness a client cannot detect. It now honours comparators, and MOCK_NO_KEYWORD_SORT=1 (plus npm run dev:mock:no-keyword-sort) reproduces a server that refuses the keyword sorts.
Testing
18 unit tests over the sort construction: both keyword directions asserted explicitly, the tiebreak present and not duplicated, custom levels capped at three, a repeated field dropped, optional-sort detection, and stripping that still leaves a valid sort.
Suite green: web 794 across 84 files, server 116, typecheck clean.
Verified in the running app, both paths
Server supporting keyword sorts: "unread first" gives a clean partition — 22 unread at positions 0–21, then read, with no unread appearing after a read row.
Server refusing them (MOCK_NO_KEYWORD_SORT=1): the folder opens with all 29 rows and no error, in newest-first order — the fallback fired silently, exactly as intended.
One bug caught during that verification and fixed: I had placed the sort helper after the useMemo that used it, which is a temporal dead zone TypeScript cannot see. It blanked the app until moved.
Newest-first was the only order, so the mail you had not read yet was wherever it happened to fall.
## What it does
Seven presets — newest, oldest, unread first, starred first, largest, by sender, by subject — or up to three levels of your own.
It covers the **Inbox alone by default**: unread-first is what people want in the folder they triage and confusing in Sent, where everything is read and the order that matters is when it went. Widen it to every folder if you want. Search keeps newest-first whatever the setting says, since a result list is already ordered by the question that was asked.
**The server does the sorting**, over the whole folder — same reason search runs there. A list sorted in the browser is sorted only as far as the browser has loaded, which on a folder of ten thousand is the first fifty and a lie about the rest.
## Two things easy to get backwards, both pinned in tests
`hasKeyword` sorts a boolean and false comes before true. So **"unread first" is `$seen` ASCENDING** while **"starred first" is `$flagged` DESCENDING** — the other way round. Getting either wrong puts exactly the mail you were looking for at the bottom.
**Every order ends with newest-first as a tiebreak.** A sort whose last level is a keyword or a subject leaves every tie undefined, and an undefined order changes between two looks at the same folder for no reason the reader can see. (No tiebreak is appended when the sort already ends on `receivedAt` — a contradictory second one would say nothing.)
## The failure this could have caused
**Sorting on a keyword is optional in RFC 8621**, and a server that will not do it fails the *whole query* rather than degrading it. So this setting could have turned the Inbox into a folder that does not open.
The refusal is caught once, the keyword levels dropped, the query retried — and nothing is said. The reader asked for an order and got the closest the server can give; a toast on every folder change would be the app complaining about its own request.
## The mock was lying
It ignored `sort` entirely and always answered newest-first — reproducing a server that silently returns a different order from the one asked for, which is the one shape of wrongness a client cannot detect. It now honours comparators, and `MOCK_NO_KEYWORD_SORT=1` (plus `npm run dev:mock:no-keyword-sort`) reproduces a server that refuses the keyword sorts.
## Testing
18 unit tests over the sort construction: both keyword directions asserted explicitly, the tiebreak present and not duplicated, custom levels capped at three, a repeated field dropped, optional-sort detection, and stripping that still leaves a valid sort.
Suite green: web 794 across 84 files, server 116, typecheck clean.
## Verified in the running app, both paths
**Server supporting keyword sorts:** "unread first" gives a clean partition — 22 unread at positions 0–21, then read, with no unread appearing after a read row.
**Server refusing them** (`MOCK_NO_KEYWORD_SORT=1`): the folder opens with all 29 rows and no error, in newest-first order — the fallback fired silently, exactly as intended.
One bug caught during that verification and fixed: I had placed the sort helper *after* the `useMemo` that used it, which is a temporal dead zone TypeScript cannot see. It blanked the app until moved.
**Merged** 2026-09-02 as coffey-labs/ihasmail@72742ef30ae8
<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.
Newest-first was the only order, so the mail you had not read yet was wherever it happened to fall.
What it does
Seven presets — newest, oldest, unread first, starred first, largest, by sender, by subject — or up to three levels of your own.
It covers the Inbox alone by default: unread-first is what people want in the folder they triage and confusing in Sent, where everything is read and the order that matters is when it went. Widen it to every folder if you want. Search keeps newest-first whatever the setting says, since a result list is already ordered by the question that was asked.
The server does the sorting, over the whole folder — same reason search runs there. A list sorted in the browser is sorted only as far as the browser has loaded, which on a folder of ten thousand is the first fifty and a lie about the rest.
Two things easy to get backwards, both pinned in tests
hasKeywordsorts a boolean and false comes before true. So "unread first" is$seenASCENDING while "starred first" is$flaggedDESCENDING — the other way round. Getting either wrong puts exactly the mail you were looking for at the bottom.Every order ends with newest-first as a tiebreak. A sort whose last level is a keyword or a subject leaves every tie undefined, and an undefined order changes between two looks at the same folder for no reason the reader can see. (No tiebreak is appended when the sort already ends on
receivedAt— a contradictory second one would say nothing.)The failure this could have caused
Sorting on a keyword is optional in RFC 8621, and a server that will not do it fails the whole query rather than degrading it. So this setting could have turned the Inbox into a folder that does not open.
The refusal is caught once, the keyword levels dropped, the query retried — and nothing is said. The reader asked for an order and got the closest the server can give; a toast on every folder change would be the app complaining about its own request.
The mock was lying
It ignored
sortentirely and always answered newest-first — reproducing a server that silently returns a different order from the one asked for, which is the one shape of wrongness a client cannot detect. It now honours comparators, andMOCK_NO_KEYWORD_SORT=1(plusnpm run dev:mock:no-keyword-sort) reproduces a server that refuses the keyword sorts.Testing
18 unit tests over the sort construction: both keyword directions asserted explicitly, the tiebreak present and not duplicated, custom levels capped at three, a repeated field dropped, optional-sort detection, and stripping that still leaves a valid sort.
Suite green: web 794 across 84 files, server 116, typecheck clean.
Verified in the running app, both paths
Server supporting keyword sorts: "unread first" gives a clean partition — 22 unread at positions 0–21, then read, with no unread appearing after a read row.
Server refusing them (
MOCK_NO_KEYWORD_SORT=1): the folder opens with all 29 rows and no error, in newest-first order — the fallback fired silently, exactly as intended.One bug caught during that verification and fixed: I had placed the sort helper after the
useMemothat used it, which is a temporal dead zone TypeScript cannot see. It blanked the app until moved.Merged 2026-09-02 as coffey-labs/ihasmail@72742ef30a
Rebuilt from: git history, session transcript.