Immutable webmail
for Stalwart.
Run it with a read-only filesystem and no volume at all: ihasmail keeps nothing of its own, because everything durable belongs to Stalwart Mail Server. What you get on top is a Gmail-class client — mail, calendars, contacts, files and filters in one responsive app that works as well on a phone as on a desktop monitor.
Features
Everything Stalwart exposes, and nothing it doesn't
One app for mail, calendars, contacts and files. Where a capability is missing from the server, the feature that needs it steps aside rather than breaking.
Mail that behaves like Gmail
Three-pane layout with a drag-to-resize splitter, conversation view, virtualised
infinite list, multi-select, drag & drop, context menus and the keyboard
shortcuts your fingers already know — j/k, e,
r/a/f, g i, /. Archive, label, move and
delete all come with Undo.
Calendar
Month, week, day and agenda views over JMAP Calendars, your own and anyone's shared with you. Recurrence with a custom rule builder, time zones, reminders, attendees with real invitations and RSVP, free/busy lookup, and Outlook-style colour categories that sync as JSCalendar. Right-click menus on events and on empty slots — and on a message, which turns it into an event with its subject, its body and its guests already in place.
Contacts & files
Address books with groups, a full contact editor, vCard and LDIF import/export and a letter index — and a way to pick recipients by reading the books rather than remembering a name. Re-import a corrected export and it updates what is there rather than duplicating it. Files browses JMAP FileNode storage with a folder tree, drag-and-drop between folders, and whole folders uploaded by dragging them in. Attach anything already there without uploading it again. Right-click anyone named in a message to add or edit them without leaving the mail.
Search you can actually type
Gmail operators work as written — from:, subject:,
has:attachment, is:unread, label:,
before:, larger: — with an advanced panel for when
you'd rather click. The query runs on the server, so it searches the whole
mailbox rather than the part the browser happens to have loaded.
A composer that keeps up
Multiple floating composers, rich text with inline images and emoji, recipient chips autocompleting from contacts, the directory and recent recipients, multiple identities with HTML signatures, templates, undo send, and scheduled send that waits in the server's queue whether or not you're still online.
Live, with no refresh button
Mail appears as it arrives. JMAP push rides an EventSource the server proxies on the page's behalf, with polling behind it for networks that cut long-lived connections. Desktop notifications, an optional sound, and an unread count in the tab title and on the favicon — so the tab tells you before you look.
Sieve filters, built visually
Right-click a message and Filter messages like this… writes a Sieve rule pre-filled from the sender or list, creating target folders on the fly — and can apply it to the mail already sitting in the folder. There's a raw script editor with server-side validation underneath.
Private by default
Remote images are blocked until you say otherwise, with a per-sender allow-list and an optional proxy that fetches them without handing the sender your address. Mail renders sanitised inside a Shadow DOM. Read receipts are offered, never sent automatically.
Nine new languages, and honest about them
German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, alongside English. All nine are marked Beta: they were made by AI and no native speaker has read a word of them. ihasmail says so in Settings, beside a link for reporting anything that reads wrongly, because the alternative is asking you to trust text nobody has checked.
The date and number format is chosen separately, so an English interface with German dates is a setting and not a compromise: every one of the ~620 locales CLDR knows about, each named in its own language and script, with your choice of date order and a 12- or 24-hour clock. Numerals follow the locale, and dates are entered through pickers in the same format, because browsers ignore the page's and use their own.
Settings follow the account, theme and all
Your default identity, locale, date and time formats, theme, labels, templates
and folder colours live in a settings.json in the account's own
JMAP Files — so they are the same wherever you sign in, private windows
included. ihasmail still stores nothing itself: the file sits in the mail store
and is backed up with it.
The look is one of those settings. Follow the system or pick a side, with accent colours over the top and dense, cozy or comfortable spacing; a new account starts on ihasmail, the palette this site is painted in. Sender HTML is left exactly as it was designed, on a light card — one setting lets mail that brings no colours of its own follow your theme instead.
Installs like an app, handles like one
A real PWA with a service worker, mobile layout, bottom tab bar and full-screen
composer. On a touchscreen it picks up the gestures your thumb already knows:
swipe a message to archive or delete it — either direction, your choice — hold
one to select it, hold a folder for its menu, pull the list to refresh, swipe
back from a conversation. Register it as your mailto: handler and
the operating system will offer ihasmail wherever it asks for a mail client —
links arrive with recipients, Cc, Bcc, subject and body already filled in.
One ihasmail, several Stalwarts
Put one installation in front of more than one server and the domain somebody signs in with chooses which. A read-only JSON file names the domains that go elsewhere; anything unlisted — and a bare username, which has no domain to map — goes to the default, so an installation that sets nothing behaves exactly as it always has. A listed domain never falls back: if its server is unreachable that sign-in fails rather than quietly landing somebody on the server their domain was routed away from. One server per person, chosen at sign-in — not several at once.
The one that matters
Immutable, in the exact sense of the word
Plenty of software calls itself stateless. The word has degrees, and most of what claims it sits a rung or two down. So here is the whole ladder — how much each one keeps of its own, and who is standing where. We are on it twice, which is the honest version.
-
a database the most there is to keep
Configuration and user state live in SQL beside the app. An instance is the schema plus everything written to it since install — so reproducing one elsewhere means reproducing that too.
RoundcubeSOGoOpen‑Xchange -
a config directory no database, still stateful
A setup wizard writes settings somewhere that has to stay read-write, so two installs of one image are not the same thing. Configuring by environment variable instead is usually possible and lifts the rung — Bulwark documents exactly that — but it means not using the wizard the product is built around.
Bulwark, as set up side by side → -
a mounted volume what "immutable" usually means
The root filesystem is read-only, which is where most people stop. The durable state has not gone anywhere though — it moved to the volume.
ihasmail, by defaultmost "immutable" deployments -
nothing the whole point
Read-only root, no volume, and no anonymous mount hiding behind one. Delete the container and nothing is lost, because there was nothing in it.
ihasmail withIMMUTABLE=1
Don't take our word for it
The claim is checkable in three commands, and worth checking on anything that makes it:
# is the root filesystem actually read-only?
docker inspect <name> --format '{{.HostConfig.ReadonlyRootfs}}'
# and is anything mounted, anonymous volumes included?
docker inspect <name> --format '{{json .Mounts}}'
The second one is the one people skip. A VOLUME line in a Dockerfile
makes Docker mount an anonymous volume whether you asked for one or not, and that
mount stays writable under --read-only — a writable
hole in a container you believed had none.
ihasmail's own image carried exactly that bug until 2.16.117. We found it by running the check above rather than by trusting the flag, which is precisely why we are suggesting you do the same to us.
Screenshots
Light or dark, desktop or phone
The app follows your system theme by default, with a toggle in the top bar and a three-way choice in Settings — the same arrangement this site uses.
Sign-in
Stalwart mailbox credentials, with TOTP where it's enabled.
Inbox & conversation — dark
Three panes, collapsed conversation, labels with colours.
Inbox & conversation — light
The same screen, following a light system preference.
Reply composer
Identities, Reply-To, rich text, signature and quoted text.
Choosing recipients
Read the address books instead of remembering a name — yours and anyone's shared with you, several at a time.
Calendar
Month view, multiple calendars, colour categories.
Files
A folder tree, drag-and-drop between folders, and folders shared by other people beneath your own.
Contacts
Address books, groups, and a full JSContact editor — yours kept apart from anyone else's.
Sieve filter builder
Also reachable from a message's right-click menu.
Mobile layout
Bottom tab bar, drawer navigation, full-screen composer.
All screenshots are taken against the built-in mock server with sample data — no real mailbox involved.
Architecture
A client, not a second mail server
ihasmail holds no state of its own. Every mailbox, event, contact and file lives in Stalwart; the server in the middle exists to keep your password out of the browser and to speak JMAP on the page's behalf.
React SPA
JMAP client, push, and the zustand stores behind each view.
ihasmail server
Node + Hono. Session cookie in, sealed Basic auth out.
Stalwart
The only source of truth. Mail, calendars, contacts, files.
What the backend is for
It authenticates against Stalwart's JMAP session endpoint and stores those
credentials sealed with a key derived from the cookie secret — the server never
persists a plaintext password, and the browser never sees one at all. From there
it proxies /api/jmap, /api/blob, /api/upload,
/api/events and /api/image, and serves the SPA under a
strict CSP.
There is no database to install, back up or migrate. Stop the container and nothing has been lost but sessions.
Stalwart 0.16 or newer
0.16 replaced the REST management API with JMAP registry objects, reshaped
FileNode, split its rights up and moved configuration into the
store. ihasmail spoke to both generations for a while, which meant every call
site had to ask which one had answered — and a wrong answer always had an older
path to fall back on, so it failed quietly instead of loudly. One supported
generation trades that for an error on the first call.
Sign-in now refuses an older server by name, once, and says plainly that the
credentials were fine. Still on 0.15 and not ready to upgrade? The last release
that runs on it is tagged stalwart-0.15-support, and it stays where
it is.
- Optional capabilities are still negotiated, not presumed
- A missing one steps the feature aside rather than erroring
- Live push via EventSource, with polling as a fallback
- A built-in mock Stalwart for development and demos
Nothing to persist
The server has exactly one path that ever writes to disk: an optional session file. Switch it off and there is nothing left to write, so the container can run with no volume and no writable root filesystem at all.
- No database to install, back up or migrate
- No volume, and no writable root
- An instance is its image plus an environment file
- Replace the container to upgrade; there is nothing to carry over
Comparison
Less to run, less to patch, less latency
Not because the code is cleverer, but because the architecture doesn't spend its time working around IMAP. Roundcube, SnappyMail and SOGo were all designed in the IMAP era and carry its shape; ihasmail is JMAP-only from the ground up.
Looking for ihasmail and Bulwark Webmail? That one is JMAP-native too, so none of the below applies — it is a different comparison and it has its own page.
| Dimension | ihasmail JMAP-native | Roundcube / SnappyMail / SOGo IMAP-based |
|---|---|---|
| round trips | One request carries several method calls, and later calls can reference earlier results — fetch a page of messages, flag one, move another, in a single exchange. | Commands pipeline, but anything that depends on a previous result still costs another round trip. The tax is per action, and it compounds over a mailbox. |
| sync | Ask what changed since a state string and get exactly that, for mail, calendars and contacts alike. It is part of the protocol, not an extension — every JMAP server has it. | IMAP does have this: CONDSTORE and
QRESYNC resynchronise a mailbox, expunges included, in one
round trip. They are optional extensions though, scoped one mailbox at a
time, and only help when server and client both implement them. |
| data model | JSON. The server has already decomposed MIME, so the client asks for the parts it wants and gets them. | Raw MIME and IMAP responses, parsed client-side. It is where a great deal of legacy webmail's bug surface and slowness actually lives. |
| own database | None. Stalwart's store is the only source of truth, so there is nothing to migrate, back up, or let drift. | Roundcube requires a database. SOGo requires one and is a second stateful system beside the mail server. SnappyMail is the exception — it keeps user data in files. |
| what an instance is | An image and an environment file. With IMMUTABLE=1 the container
runs read-only with no volume, so two hosts given the same two things are
running the same thing. Upgrading is replacing the container. |
An image plus whatever the database and the config directory have accumulated since install. Reproducing an instance means reproducing that state as well — which is why upgrades are a migration rather than a replacement. |
| constrained networks | What JMAP was designed for. Batching and delta sync are the same mechanisms that make a phone on a bad connection feel workable. | Efficiency comes from extensions bolted onto a protocol that predates the problem — or, in SOGo's case, from carrying Exchange ActiveSync alongside it. That buys Outlook and stock mobile clients, which is worth having; it is also a second protocol to implement and keep secure, for something JMAP already does on its own. |
Where that shows up
- Fewer moving parts. No database of its own means nothing to migrate, back up, or reconcile. You administer Stalwart, and that is the whole list.
- Lower latency per action. The round-trip tax is the reason JMAP was built. ihasmail gets the win structurally, just by being on the protocol.
- Less to audit. One protocol, no plugin surface, no compatibility layer for a second one. Roundcube's plugin ecosystem is its greatest strength and its largest attack surface at the same time.
- Nothing to emulate. No pretending to be an Exchange server for the benefit of a client that wanted one.
Stack
What it's built on
Small, current, and boring where boring is a virtue.
| Layer | Choice | Why |
|---|---|---|
| web/ | Vite + React 19 + TypeScript | The SPA: src/jmap (client, push, types), src/store (zustand), src/views, src/lib. |
| server/ | Node + Hono | A thin proxy and session holder. Small enough to read in an afternoon. |
| protocol | JMAP | No IMAP, no POP3, no SMTP fallback path. One protocol, used properly. |
| storage | Stalwart | There is no second store. Nothing to keep in sync, nothing to reconcile. |
| sanitiser | DOMPurify + Shadow DOM | Sender HTML renders isolated from the app's own styles and scripts. |
| deployment | Docker | One container, configured entirely by environment variables. |
| licence | AGPL-3.0-or-later | Run it as a service for other people and they get the source too. |
Security & privacy
Your password never reaches the browser
A webmail client sits between someone's mailbox and the open internet. These are the parts that decision touches.
In the app
- Server-side sessions with per-session encrypted upstream credentials — no password in the browser, ever
- httpOnly, SameSite cookies; CSRF header plus
Sec-Fetch-Sitechecks - Strict Content-Security-Policy, sandboxed blob downloads
- Sender HTML sanitised with DOMPurify inside a Shadow DOM
- Remote images blocked by default, with an SSRF-safe proxy when you allow them
- Login rate limiting and the usual security headers
Self-service, in Settings
Change your password and manage app passwords — a separate one per device, revocable on its own, so losing a phone costs you that phone rather than every session you have.
Read receipts are worth calling out: when a sender asks for one, ihasmail
offers to send a real RFC 8098 report — never automatically. Bulk mail,
mailing lists and anything marked Auto-Submitted aren't offered one
at all, and a receipt aimed somewhere other than the sender says so first.
License
AGPL-3.0-or-later, and why it changed
ihasmail was relicensed from GPL-3.0 to AGPL-3.0 in August 2026. 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: anyone running a modified ihasmail for other people has to offer them its source, which the GPL alone does not require.
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, so the people using your
instance are told where the code they are actually running can be found.
Free software, no feature held back
There is no paid tier, no enterprise edition and nothing switched off behind a licence key. Copyright © 2026 Coffey Labs.
About
Built for one mailbox, then everyone else's
ihasmail exists because Stalwart is an excellent mail server with a genuinely modern protocol, and the web clients pointed at it were mostly IMAP clients wearing a JMAP hat. Building JMAP-first from the ground up turns out to change what the client can do: conversations, push, server-side scheduled send, free/busy lookup and Sieve round-tripping all stop being bolted on.
What runs today is a ground-up rewrite, against a live Stalwart 0.16.20. The earlier FastAPI/HTMX prototype has been removed entirely — only the logo survived.
Builds say what they are: 2026.8.30+pr129 is the date of the commit
the image was built from, then the pull request it arrived through — or a short
SHA, +g1fa6578, where a commit arrived some other way. The date is
the commit's own rather than the build's, so rebuilding an old commit gives the
version it had the first time, and a build reporting 0.0.0 is one
nobody told what it was.
The number says nothing about Stalwart, deliberately. It used to:
2.16.x carried the generation it targeted in the middle, which
leaves nowhere to go once Stalwart reaches 1.0 — 2.1 sorts below the
2.16 already deployed, so every upgrade would read as a
downgrade. Which Stalwart a build needs is stated where it can be precise, in the
install
docs, instead of compressed into one digit.
A companion project, stalwart-migrator, handles the 0.15 → 0.16 upgrade that ihasmail now requires: it checkpoints every phase, refuses to start on the things that cannot be fixed mid-migration, and validates the server afterwards. That path is treacherous by hand — the store is migrated in place with no way back, and Stalwart's own converter drops settings without saying so. The live instance made the jump in eight seconds of downtime with nothing lost.
ihasmail is written and maintained by Coffey Labs. Issues, feature requests and patches are all welcome on GitHub.