ihasmail

Licence: AGPL-3.0-or-later Requires Stalwart 0.16 or newer; tested against 0.16.19 Documentation: docs.ihasmail.org by LINUXexpert.org

# ihasmail **A fast, friendly, Gmail-class webmail for [Stalwart Mail Server](https://stalw.art) โ€” built on JMAP, from the ground up.** Mail, calendars, contacts, files and filters in a responsive single-page app that works equally well on a desktop monitor and a phone. It talks only JMAP (plus Stalwart's blob/upload/EventSource endpoints) โ€” no IMAP, no SMTP, no database. | | | | --- | --- | | ๐ŸŒ **[ihasmail.org](https://ihasmail.org)** | What it is, what it looks like, the full feature list | | ๐Ÿ“˜ **[docs.ihasmail.org](https://docs.ihasmail.org)** | [Installing](https://docs.ihasmail.org/install/) ยท [Configuring](https://docs.ihasmail.org/configure/) ยท [Using it](https://docs.ihasmail.org/using/) ยท [Shortcuts](https://docs.ihasmail.org/shortcuts/) ยท [Rebranding](https://docs.ihasmail.org/rebranding/) ยท [Troubleshooting](https://docs.ihasmail.org/troubleshooting/) | | ๐Ÿงช **[KNOWN-ISSUES.md](KNOWN-ISSUES.md)** | What was verified live, and where Stalwart departs from a spec | | ๐Ÿ›ฃ **[ROADMAP.md](ROADMAP.md)** | What ihasmail does not do, and why | This file is for people working *on* ihasmail. Everything about running it lives in the docs. ## Screenshots *Taken against the built-in mock server (`npm run dev:mock`) with sample data โ€” no real mailbox involved.* | | | | --- | --- | | **Inbox & conversation (dark)** ![Inbox, dark theme](docs/screenshots/inbox-dark.jpg) | **Inbox & conversation (light)** ![Inbox, light theme](docs/screenshots/inbox-light.jpg) | | **Composer** ![Composer](docs/screenshots/compose.jpg) | **Calendar** ![Calendar](docs/screenshots/calendar.jpg) | | **Contacts** ![Contacts](docs/screenshots/contacts.jpg) | **Sieve filter builder** ![Filters](docs/screenshots/filters.jpg) | More, including the mobile layout, on [ihasmail.org](https://ihasmail.org/#screenshots). ## What's in it - **Mail** โ€” three-pane Gmail-style layout, conversation view, virtualised list, labels, undo, Gmail search operators and keyboard shortcuts, Sieve rules from a message's context menu, sanitised HTML with remote images blocked, read receipts, invitations and RSVP, multi-composer rich-text editing with signatures, scheduled send and undo send - **Calendar** โ€” JMAP Calendars / JSCalendar: month/week/day/agenda, recurrence, attendees and free-busy, colour categories - **Contacts** โ€” JMAP Contacts / JSContact: address books, groups, full editor, vCard import/export - **Files** โ€” JMAP FileNode: browse, upload, download, rename, move, delete - **Settings that follow the account**, not the browser โ€” kept in a `settings.json` in the account's own JMAP Files, so ihasmail itself stays stateless - **Platform** โ€” installable PWA, Web Push with ihasmail closed, `mailto:` handler, no credentials in the browser, strict CSP, SSRF-safe image proxy The long version is on [ihasmail.org](https://ihasmail.org/#features); how to drive each one is in [Using ihasmail](https://docs.ihasmail.org/using/). ## Requires Stalwart 0.16 or newer Sign-in refuses anything older, by name. 0.16 replaced the REST management API with JMAP registry objects, changed the shape of `FileNode`, split its rights up and moved configuration into the store; supporting both generations meant a wrong guess had somewhere to fall back to, so it failed *quietly* โ€” and that reached production. With one supported generation a wrong guess is a loud error on the first call. - Still on 0.15? 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). - Upgrading? [stalwart-migrator](https://github.com/LINUXexpert-org/stalwart-migrator) does it in place, checkpointing every phase and validating afterwards. The live instance moved 0.15.5 โ†’ 0.16.19 with eight seconds of downtime and nothing lost. ## Quick start (Docker) ```bash cp .env.example .env # edit: STALWART_URL=https://mail.example.com and APP_SECRET=$(openssl rand -base64 48) docker compose up --build -d # โ†’ http://localhost:8080 (put Caddy/nginx in front for TLS; see Caddyfile.example / nginx.example.conf) ``` Users sign in with their Stalwart mailbox credentials. **An account with two-factor authentication needs an app password**, created in Stalwart's own settings โ€” Stalwart accepts a TOTP code only through an OAuth flow and offers no password grant, so no client holding a username and password can exchange them plus a code for a token. Full instructions, TLS, and every environment variable: [Installing](https://docs.ihasmail.org/install/) ยท [Configuring](https://docs.ihasmail.org/configure/). ### Running immutably The server writes to exactly one path, the optional `SESSION_FILE`. Clear it and there is nothing left to write, so the container can run with no writable filesystem at all: ```bash docker run --read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE= ... ``` `IMMUTABLE=1` is an assertion the server checks at startup rather than a switch that changes what it does: it refuses to start if `SESSION_FILE` is still set, or if the filesystem it is installed on turns out to be writable after all. Without it the same misconfiguration is silent โ€” sessions are held in memory and persisting them is best-effort, so a read-only `/data` costs one warning at the first sign-in and nothing else until the instance is replaced and everyone is signed out. That sign-out is the standing cost of this mode today, since sessions have nowhere to live across a restart. Removing it means moving the session upstream into a token Stalwart itself issues and can revoke, which is what the OAuth work in [ROADMAP.md](ROADMAP.md) is for. ## Architecture ``` browser โ”€โ”€(same-origin /api/*)โ”€โ”€โ–บ ihasmail server (Node + Hono) โ”€โ”€(JMAP over HTTPS)โ”€โ”€โ–บ Stalwart React SPA โ€ข session cookie โ‡„ Basic auth JMAP client + stores โ€ข /api/jmap, /api/blob, /api/upload, /api/events (SSE), /api/image ``` - `web/` โ€” Vite + React 19 + TypeScript SPA. `src/jmap` (client, push, types), `src/store` (zustand: session, mail, compose, contacts, calendar, files, sieve, settings), `src/views`, `src/lib` (sanitiser, search parser, Sieve codec, locale-aware dates, vCard, โ€ฆ). - `server/` โ€” Node/Hono backend: authenticates against Stalwart's JMAP session endpoint, seals the credentials with a key derived from the cookie secret, proxies JMAP/blob/SSE, serves the SPA under a strict CSP. `src/mock/` is an in-memory fake Stalwart for development and demos. Capabilities used: `core`, `mail`, `submission`, `vacationresponse`, `sieve`, `contacts`(+`parse`), `calendars`(+`parse`), `principals`(+`availability`), `quota`, `blob`, `filenode`, EventSource push, plus Stalwart's own `urn:stalwart:jmap` (read-only). Features degrade gracefully when one is missing. ## Development Requirements: Node โ‰ฅ 20.10 (22 recommended), npm โ‰ฅ 10. ```bash npm install npm run dev # real Stalwart (STALWART_URL in .env) โ€” server :8080, Vite :5173 npm run dev:mock # built-in mock Stalwart (demo@example.com / demo), mock on :8788 npm run dev:mock:no-future-release # mock that advertises FUTURERELEASE and drops every hold npm run typecheck # tsc for both packages npm test # vitest (web) + node:test (server) npm run build # web/dist + server/dist npm start # serve the production build ``` Open http://localhost:5173 in dev, or http://localhost:8080 for the production build. Running it for real is covered in [Installing](https://docs.ihasmail.org/install/) and [Configuring](https://docs.ihasmail.org/configure/). ### The mock An in-memory fake Stalwart 0.16 โ€” enough JMAP to develop and demo against without a real mailbox. It reproduces the things a naive fake would get wrong, because each cost a live debugging session: `urn:stalwart:jmap` advertised **per-account** rather than session-level, identity signatures capped at 2047 **bytes**, and `CalendarEvent/set` speaking Stalwart's vocabulary rather than RFC 8984's. Two switches: `MOCK_NO_FUTURE_RELEASE=1` advertises FUTURERELEASE and then drops every hold; `MOCK_NO_REGISTRY=1` omits the Stalwart capability so the sign-in refusal can be tested. ### Version numbers `ihasmail v2.16.84` โ€” `2` is ihasmail's own major, `16` the Stalwart generation this build targets, `84` the pull request the commit came from. The first two live in the root `package.json`; the third comes from git at build time, since it does not exist until the PR has merged. A commit that did not arrive through a PR carries the last number plus its short SHA โ€” `2.16.84+g1fa6578`. ```bash node scripts/version.mjs # the version for the current checkout docker build --build-arg IHASMAIL_VERSION="$(node scripts/version.mjs)" -t ihasmail:2.16 . ``` `.dockerignore` excludes `.git` deliberately, so an image build cannot work this out for itself โ€” pass it in. Left out, the build falls back to the base version from `package.json`, so a version with no PR number means whoever built the image did not pass one. ### Deploying [`deploy.example.sh`](deploy.example.sh) is a single-host Docker deploy: it fetches, refuses anything held back by `.deploy-hold`, shows what is about to be introduced and asks, rebuilds with the right version baked in, replaces the container, waits for healthy, then prunes all but the newest `IHASMAIL_KEEP_VERSIONS` images โ€” never the one actually running. ```bash ./deploy.sh # origin/main, asks before shipping new commits ./deploy.sh --dry-run # run the guards and stop ./deploy.sh v2.16.84 --yes # a named ref, no prompt (there is no tty over ssh) ``` `--yes` does not override a hold; clearing one means deleting its line. ## Contributing [CONTRIBUTING.md](CONTRIBUTING.md) ยท [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) ยท [SECURITY.md](SECURITY.md) โ€” please report vulnerabilities privately. ## License Copyright (C) 2026 LINUXexpert.org โ€” AGPL-3.0-or-later. See [LICENSE](LICENSE). ihasmail was relicensed from GPL-3.0 to AGPL-3.0 on 2026-08-25: webmail is nearly always run as a network service rather than handed to anyone as a binary, and the AGPL's section 13 closes that gap. That offer has to point at *your* source, not this one. If you run a modified ihasmail, set `SOURCE_URL` to your own repository โ€” the sign-in page and Settings โ€บ About both show it. See [Rebranding](https://docs.ihasmail.org/rebranding/).