Four modules are not where their names suggest. I read each file rather than trusting the filename, having already been caught out by archiveDate last time:
appFolder is not a mailbox. It is the ihasmail folder in JMAP Files, holding signature images and synced settings. Stays flat.
format contains no text formatting. It re-exports the date and clock formatters, so it belongs with dates/datetime, not text/.
preview is the file viewer deciding what it can show without downloading; source is where to point someone asking for this instance's AGPL source. Neither is text.
notify is not Web Push. It is the tab title, favicon badge and new-mail sound — which is why it sits beside webpush rather than under sw/.
threadScroll also stays flat: it decides where a conversation opens, which is view state, not a gesture, and input/ is only honest if everything in it interprets something the reader did.
counter and vacation were module-level lets written from both the fixture builders and the handlers. A module can export a let and importers see it update, but they cannot assign to it — so both became containers, seq.counter and vacationBox.current. Seven call sites.
recordEmailChange, broadcast, sseClients, checkOtp sat in the HTTP section, but the handlers call them and index.ts imports the handlers. Leaving them put is a cycle, so they became events.ts and auth.ts rather than being dragged into data.ts, which is fixtures.
account is still exported from index.ts — account.test.ts and login-guard.test.ts reach for mock.account and mock.server.
Translations: adds none. No user-visible strings in either commit.
Testing:typecheck exit 0, npm test exit 0 (135 web files / 1,292 tests, 228 server), build exit 0. The mock was also run, not just compiled: it boots and listens, /.well-known/jmap returns a session, and a POST to /jmap/ answers Mailbox/get with the nine seeded folders and Email/query with the seeded messages — which exercises handlers, engine and data together.
Two refactors, one theme, no behavior change. Separate commits — review them independently.
## 1. `web/src/lib`: 66 flat modules → 42
| Folder | Modules |
| --- | --- |
| `lib/mailbox/` | `archiveDate`, `emptyFolder`, `folderMove`, `labelTree`, `mailboxName`, `mailboxRoute` |
| `lib/sieve/` | `sieve`, `sieveApply`, `sieveFolders` |
| `lib/input/` | `keyboard`, `swipe`, `touch`, `listSelection`, `dropUpload` |
| `lib/notify/` | `notify`, `webpush`, `webpushEnable` |
| `lib/sw/` | `swCache`, `swFacts`, `staleBuild` |
| `lib/text/` | `html`, `markdown`, `text`, `emlName` |
**Four modules are not where their names suggest.** I read each file rather than trusting the filename, having already been caught out by `archiveDate` last time:
- **`appFolder` is not a mailbox.** It is the `ihasmail` folder in JMAP *Files*, holding signature images and synced settings. Stays flat.
- **`format` contains no text formatting.** It re-exports the date and clock formatters, so it belongs with `dates`/`datetime`, not `text/`.
- **`preview` is the file viewer** deciding what it can show without downloading; **`source`** is where to point someone asking for this instance's AGPL source. Neither is text.
- **`notify` is not Web Push.** It is the tab title, favicon badge and new-mail sound — which is why it sits beside `webpush` rather than under `sw/`.
`threadScroll` also stays flat: it decides where a conversation opens, which is view state, not a gesture, and `input/` is only honest if everything in it interprets something the reader did.
## 2. `server/src/mock/index.ts`: 1,545 lines → seven files
It already carried `/* ---------- data ---------- */` markers, so the seams were drawn; this turns them into files.
| File | Lines | Holds |
| --- | --- | --- |
| `mock/config.ts` | 51 | env constants, `account`, `state` |
| `mock/data.ts` | 410 | fixtures and their builders |
| `mock/engine.ts` | 442 | generic JMAP machinery — get/set, filters, patches, refs, limits |
| `mock/handlers.ts` | 455 | the `Method/name` dispatch table |
| `mock/events.ts` | 26 | SSE fan-out, `Email/changes` ring buffer |
| `mock/auth.ts` | 11 | `checkOtp` |
| `mock/index.ts` | 195 | HTTP routing, session document, listen |
**Two things could not simply move:**
- **`counter` and `vacation`** were module-level `let`s written from both the fixture builders and the handlers. A module can export a `let` and importers see it update, but they cannot assign to it — so both became containers, `seq.counter` and `vacationBox.current`. Seven call sites.
- **`recordEmailChange`, `broadcast`, `sseClients`, `checkOtp`** sat in the HTTP section, but the handlers call them and `index.ts` imports the handlers. Leaving them put is a cycle, so they became `events.ts` and `auth.ts` rather than being dragged into `data.ts`, which is fixtures.
`account` is still exported from `index.ts` — `account.test.ts` and `login-guard.test.ts` reach for `mock.account` and `mock.server`.
**Translations:** adds none. No user-visible strings in either commit.
**Testing:** `typecheck` exit 0, `npm test` exit 0 (135 web files / 1,292 tests, 228 server), `build` exit 0. The mock was also **run**, not just compiled: it boots and listens, `/.well-known/jmap` returns a session, and a POST to `/jmap/` answers `Mailbox/get` with the nine seeded folders and `Email/query` with the seeded messages — which exercises `handlers`, `engine` and `data` together.
**Merged** 2026-09-15 as coffey-labs/ihasmail@d0b13272f3b1
<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.
Two refactors, one theme, no behavior change. Separate commits — review them independently.
1.
web/src/lib: 66 flat modules → 42lib/mailbox/archiveDate,emptyFolder,folderMove,labelTree,mailboxName,mailboxRoutelib/sieve/sieve,sieveApply,sieveFolderslib/input/keyboard,swipe,touch,listSelection,dropUploadlib/notify/notify,webpush,webpushEnablelib/sw/swCache,swFacts,staleBuildlib/text/html,markdown,text,emlNameFour modules are not where their names suggest. I read each file rather than trusting the filename, having already been caught out by
archiveDatelast time:appFolderis not a mailbox. It is theihasmailfolder in JMAP Files, holding signature images and synced settings. Stays flat.formatcontains no text formatting. It re-exports the date and clock formatters, so it belongs withdates/datetime, nottext/.previewis the file viewer deciding what it can show without downloading;sourceis where to point someone asking for this instance's AGPL source. Neither is text.notifyis not Web Push. It is the tab title, favicon badge and new-mail sound — which is why it sits besidewebpushrather than undersw/.threadScrollalso stays flat: it decides where a conversation opens, which is view state, not a gesture, andinput/is only honest if everything in it interprets something the reader did.2.
server/src/mock/index.ts: 1,545 lines → seven filesIt already carried
/* ---------- data ---------- */markers, so the seams were drawn; this turns them into files.mock/config.tsaccount,statemock/data.tsmock/engine.tsmock/handlers.tsMethod/namedispatch tablemock/events.tsEmail/changesring buffermock/auth.tscheckOtpmock/index.tsTwo things could not simply move:
counterandvacationwere module-levellets written from both the fixture builders and the handlers. A module can export aletand importers see it update, but they cannot assign to it — so both became containers,seq.counterandvacationBox.current. Seven call sites.recordEmailChange,broadcast,sseClients,checkOtpsat in the HTTP section, but the handlers call them andindex.tsimports the handlers. Leaving them put is a cycle, so they becameevents.tsandauth.tsrather than being dragged intodata.ts, which is fixtures.accountis still exported fromindex.ts—account.test.tsandlogin-guard.test.tsreach formock.accountandmock.server.Translations: adds none. No user-visible strings in either commit.
Testing:
typecheckexit 0,npm testexit 0 (135 web files / 1,292 tests, 228 server),buildexit 0. The mock was also run, not just compiled: it boots and listens,/.well-known/jmapreturns a session, and a POST to/jmap/answersMailbox/getwith the nine seeded folders andEmail/querywith the seeded messages — which exerciseshandlers,engineanddatatogether.Merged 2026-09-15 as coffey-labs/ihasmail@d0b13272f3
Rebuilt from: git history, session transcript.