~/LINUXexpert$
Articles

Announcing ihasmail: a JMAP Webmail Built for Stalwart

I have been self-hosting my own mail for longer than I care to admit, and the part that never got better was the web client. The server side moved on — Stalwart gave me mail, calendars, contacts, filters and file storage in a single binary with a sane config — but the browser side was still asking me to bolt an IMAP-era webmail onto it and pretend the two were designed for each other. So I wrote one that wasn’t.

ihasmail is a JMAP-first webmail client for Stalwart: github.com/LINUXexpert-org/ihasmail. It is GPLv3, it runs as a small Node service in front of your existing server, and it speaks exactly one protocol to talk to it.

Why not just run Roundcube?

Because of what the seams cost you. A traditional webmail stack talks IMAP for mail, SMTP for sending, CalDAV for calendars, CardDAV for contacts, and keeps its own database for everything those protocols can’t express — flags it caches, addresses it has seen, preferences with nowhere else to live. Every one of those is a separate connection, a separate auth path, and a separate opportunity for the client’s idea of your mailbox to drift from the server’s.

JMAP collapses that. It is one HTTP/JSON protocol covering mail, submission, calendars, contacts, filters, quota and blobs, with batched requests, real change tracking, and server push. Stalwart implements it natively. ihasmail talks only JMAP, plus Stalwart’s blob/upload and EventSource endpoints — no IMAP, no SMTP, and no database of its own. There is nothing to back up on the client side, because the client stores nothing. Your mail server is the only source of truth.

That constraint is the whole design. It is also why the thing can do a batched “archive these forty messages and update the counts” round trip in one request, and why a change made in Thunderbird shows up in the browser without a refresh.

What you actually get

The goal was Gmail-class, not Gmail-lite. Mail is a three-pane layout with the reading pane right, bottom, or off, a drag-to-resize splitter, conversation view with collapsed messages and quoted-text folding, and density and theme settings that stick. The message list is virtualised and infinitely scrolling, with multi-select, drag and drop to folders, right-click context menus, and the Gmail keyboard shortcuts already in your fingers — j/ke to archive, # to delete, r/a/fg i/ to search. Archive, move, label and delete all have working Undo.

The composer supports multiple floating windows, rich text with inline pasted images, plain-text mode, recipient autocomplete drawn from your contacts, the directory, and recent recipients, multiple identities with HTML signatures, templates, attachment upload with progress, an attachment reminder, autosaved drafts, and undo send.

Search takes the operators you already know: from:to:subject:has:attachmentis:unreadis:starredin:label:before:after:larger:smaller:, with an advanced panel for when you would rather click.

Then there is everything that isn’t mail, which is the part self-hosters usually have to solve separately:

  • Calendar — month, week, day and agenda views over JMAP Calendars/JSCalendar. Multiple calendars with colours and sharing, drag-to-create events, recurrence with a custom rule builder, time zones, reminders, attendee invitations with RSVP, and free/busy lookup against the directory. Outlook-style colour categories are stored as JSCalendar categories, so they sync rather than living in a browser.
  • Contacts — address books over JSContact, a full contact editor, groups, vCard import and export, letter index and search.
  • Files — browse, upload by drag and drop, download, rename, move, delete, backed by Stalwart’s FileNode store.
  • Sieve filters — a visual rule builder that round-trips to an actual Sieve script, with a raw editor and server-side validation for when the builder isn’t enough.

My favourite piece is small and came straight out of daily annoyance: right-click a message, choose Filter messages like this…, and you get a Sieve rule pre-filled from the sender or the mailing list, with the option to create the target folder on the spot — and to apply the rule immediately to the messages already sitting in the folder. Writing a filter that only helps with tomorrow’s mail always felt like half a feature.

It installs as a PWA, and the mobile layout is a real layout — bottom tab bar, drawer navigation, full-screen composer — not a desktop grid squeezed onto a phone.

The security posture

This is a webmail client, so it is worth being explicit about where the credentials live: not in the browser. You sign in against the small Node/Hono server, it authenticates to Stalwart’s JMAP session endpoint, and it holds your upstream credentials sealed with a key derived from the cookie secret. The server never persists a plaintext password, and the SPA never sees one. Sessions are httpOnly and SameSite, with a CSRF header and Sec-Fetch-Site checks on top.

Message HTML is sanitised with DOMPurify and rendered inside a Shadow DOM under a strict CSP. Remote images are blocked by default with a per-sender allow-list, and an optional SSRF-safe image proxy if you want Gmail-style loading without leaking your IP to every newsletter you open. Downloads are sandboxed blobs, and logins are rate limited. TOTP works — Stalwart accepts the code as password$code, and there is a field for it on the sign-in form.

Trying it without touching your mail

There is a mock Stalwart built into the repo — an in-memory fake server with sample data — so you can run the whole thing locally and click around before you point it at anything real. You need Node 20.10 or newer:

git clone https://github.com/LINUXexpert-org/ihasmail
cd ihasmail
npm install
npm run dev:mock

That brings up the mock on :8788, the server on :8080, and Vite on :5173. Open http://localhost:5173 and sign in as [email protected] / demo. No mailbox involved.

When you want it in front of your own server, it is a container and two environment variables:

cp .env.example .env
# STALWART_URL=https://mail.example.com
# APP_SECRET=$(openssl rand -base64 48)
docker compose up --build -d

It listens on :8080; put Caddy or nginx in front for TLS, and there are example configs for both in the repo. Users sign in with their ordinary Stalwart mailbox credentials — there are no accounts to provision in ihasmail, because ihasmail has no accounts.

Where it honestly stands

This is the 2.0 rewrite. The earlier FastAPI/HTMX prototype is gone entirely — only the logo survived — and the current code is in QA against a live Stalwart 0.15.5 server, yeah I’ll get around to updating that soon, whole new script incoming for an upgrade in place. The core mail flows are verified there; everything else is verified against the mock and getting a live pass. Three things I know are still rough:

  • HTML signatures. Stalwart caps identity signatures at 2 KB. ihasmail compacts pasted HTML, moves images into Files, and falls back to keeping the full signature in Files behind a short marker. Implemented, not yet confirmed end to end on the live server.
  • Files. The Stalwart build I test against is older than main and rejects isTopLevel/parentId filters on FileNode/query, so the client lists all nodes and builds the tree itself. Upload, rename, move and delete still need a live pass.
  • Recurring events. Edits, colours and deletions apply to the whole series; per-occurrence overrides aren’t supported by the server yet.

Not yet built, and on the list: snooze and scheduled send, read receipts, S/MIME and OpenPGP, and translations — the strings are English-only today. Self-service password and 2FA management stays in Stalwart’s own account portal, where it belongs.

Features degrade gracefully when a capability is missing, so an older or differently configured Stalwart should still give you a working client, minus the parts it can’t serve.

Take it

ihasmail is GPL-3.0-or-later. Run it, fork it, file issues against it — especially if you have a Stalwart deployment that looks different from mine, because that is exactly the testing I can’t do alone.

github.com/LINUXexpert-org/ihasmail