Self-service credentials, plus the account-locale fix, server info and a theme toggle
ihasmail
A fast, friendly, Gmail-class webmail for Stalwart Mail Server — built on JMAP, from the ground up.
ihasmail is a JMAP-first web client: mail, calendars, contacts, files, filters and every other modern feature Stalwart exposes, 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.
Status: 2.0 rewrite, in QA against a live Stalwart 0.15.5 server. The previous FastAPI/HTMX prototype has been removed entirely (only the logo survived).
Screenshots
All screenshots are taken against the built-in mock server (npm run dev:mock) with sample data — no real mailbox involved.
Features
- Gmail-style three-pane layout (reading pane right/bottom/off, drag-to-resize splitter in both orientations, quick layout switch in the list menu), conversation view with collapsed messages and "show quoted text", dense/cozy/comfortable density, light/dark/system theme with accent colours
- Virtualised, infinitely-scrolling message list; multi-select (click, ⇧-click, ⌃-click), drag & drop to folders, right-click context menus, hover actions, Gmail keyboard shortcuts (
j/k,e,#,r/a/f,g i,/,?…) - Archive / delete / spam / star / mark read / move / labels (IMAP keywords with colours) with Undo
- "Filter messages like this…" from the message context menu: creates a Sieve rule pre-filled from the sender/list (target folders can be created on the fly), and can apply it immediately to the existing messages in the folder (evaluated client-side, actions applied via JMAP)
- Safe HTML rendering: DOMPurify sanitisation inside a Shadow DOM, remote images blocked by default with a per-sender allow-list and an optional privacy image proxy (like Gmail's)
- Messages sit on a light card by default, untouched as the sender designed them. Appearance › Apply the theme to messages too lets them follow the app's light/dark theme instead — plain-text mail always does, and with the option on so does HTML mail that brings no colours of its own; mail that styles itself is still left alone
- Attachments: previews for images/PDF/text, download all, inline
cid:images,.emlexport, Show original, header viewer - Invitations:
.icsparts render as an invite card with Yes/Maybe/No RSVP (viaCalendarEvent/parse+ iTIP);.vcfparts offer Add to contacts;List-Unsubscribeone-click - Right-click anyone named in a message — sender, To, Cc, Bcc, Reply-To — to add them to the address book (the contact editor opens prefilled, with the display name split into first/last), edit them if they are already known, write to them, or copy the address
- Search with Gmail operators (
from:,to:,subject:,has:attachment,is:unread,is:starred,in:,label:,before:,after:,larger:,smaller:…) plus an advanced-search panel - Composer: multiple floating/minimised/maximised composers, rich-text editor (formatting, lists, links, colours, images pasted/dropped inline, emoji), plain-text mode, recipient chips with autocomplete from contacts, the directory (GAL) and recent recipients, multiple identities with HTML signatures, Cc/Bcc, priority, read-receipt request, templates/canned responses, attachment upload with progress, drag & drop, attachment reminder, undo send, autosaved drafts, reply/reply-all/forward with quoting and inline images preserved
- Live updates via JMAP push (EventSource proxied server-side) with polling fallback; desktop notifications, sound, title/favicon unread badge
- A–Z folder list with Inbox pinned on top (other special folders mixed in), subfolders nested and collapsed by default with chevrons in their own gutter so every icon lines up; unread folders are bold (a parent is bold when a subfolder has unread mail); right-click a folder to mark it read including subfolders, create/rename/hide/share/empty, quota bar, Outlook-style module bar (Mail · Calendar · Contacts · Files) at the bottom of the pane, multi-account switching for shared accounts
Calendar (JMAP Calendars / JSCalendar)
- Month / week / day / agenda views, mini calendar, multiple calendars with colours, show/hide, create/edit/share calendars
- Create events by click or drag, edit everything: all-day, time zones, recurrence (presets + custom rule builder), location, meeting link, description, reminders, status/privacy/free-busy, colour
- Attendees with invitations (
sendSchedulingMessages), RSVP, and free/busy lookup viaPrincipal/getAvailability - Right-click menus on events (open, edit, duplicate, colour, category, delete) and on empty slots/days (new event here, go to day/week)
- Outlook-style colour categories: named colours managed in Settings, assigned from the context menu or editor; stored as JSCalendar
categories(+color) so they sync
Contacts (JMAP Contacts / JSContact)
- Address books (create/rename/share/default), contact list with search and letter index, full contact editor (names, emails, phones, addresses, org/title, birthday, website, notes, photo), groups, vCard import/export, compose-to-contact
Files (JMAP FileNode)
- Browse folders, upload (drag & drop), download, create folders, rename, move, delete
Settings
- Dates & times: language/region (every one of the ~620 locales CLDR has data for, each named in its own language and script), date order (locale default,
22.11.2025,22/11/2025,11/22/2025or ISO2025-11-22) and 12h/24h clock, applied everywhere — message list and headers, calendar, contacts, files, sessions. The default comes from the locale configured for the account in Stalwart (x:AccountSettings/get, falling back tox:Account/get), and from the browser where the server will not say; POSIX forms are normalised (de_DE.UTF-8→de-DE) and script modifiers preserved (sr_RS@latin→sr-Latn-RS). Numerals follow the locale (٢٢.١١.٢٠٢٥forar-EG), except under ISO 8601, which pins date and clock to Latin digits. Dates are entered through custom pickers in the same format (browsers render<input type="date">in their own locale and ignore the page's), with a calendar popover, a time list, keyboard navigation, and lenient typing —22.11.,221125,6:23pmand bare ISO all parse - Self-service credentials in Settings › Security: change your password, manage app passwords (a separate password per mail app or device, revocable on its own), and turn two-factor authentication on or off by scanning a QR code. Enrolment codes are verified before anything is stored, so a mistyped key cannot lock you out, and switching 2FA on moves this browser's session onto a dedicated app password instead of signing you straight back out. Works against both Stalwart generations: the
x:AccountPassword/x:AppPasswordregistry objects on 0.16+, and the/api/account/authREST endpoint on 0.15.x (the latter confirmed live) - Light and dark follow the system by default, with a toggle in the top bar for flipping between them and a three-way choice in Settings › Appearance
- Identities & signatures, Sieve filters (visual rule builder that round-trips to a Sieve script, plus a raw script editor with server-side validation), out-of-office (
VacationResponse), folders, labels, templates, notifications, calendar defaults, sessions (sign out other devices), keyboard shortcuts, import/export of settings
Platform
- Installable PWA (manifest + service worker), mobile layout with bottom tab bar, drawer navigation, full-screen composer, FAB
- Default mail app: register ihasmail as the browser's handler for
mailto:links from Settings › General (registerProtocolHandler; needs HTTPS and a browser that supports it — Safari does not). Installed as an app it also declaresprotocol_handlersin the manifest, which is what lets the operating system offer ihasmail wherever it asks for a mail client. Links arrive with recipients, Cc, Bcc, subject and body filled in - About reports the Stalwart generation ihasmail detected (0.16+ or older) and the edition where the server gives one. Stalwart does not publish a version number to clients, so no version is shown rather than a made-up one
- Security: no credentials in the browser (server-side session with per-session encrypted upstream credentials), httpOnly SameSite cookies, CSRF header + Sec-Fetch-Site checks, strict CSP, sandboxed blob downloads, SSRF-safe image proxy, login rate limiting, security headers
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 stores: session, mail, compose, contacts, calendar, files, sieve, settings),src/views(mail, compose, calendar, contacts, files, settings),src/lib(sanitiser, search parser, Sieve codec, dates and locale-aware formatting, vCard, …).server/— tiny Node/Hono backend: authenticates against Stalwart's JMAP session endpoint, stores the credentials sealed with a key derived from the cookie secret (the server never persists plaintext passwords), proxies JMAP/blob/SSE calls, serves the SPA with a strict CSP. Also containssrc/mock/— an in-memory fake Stalwart for local development and demos.
Stalwart 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, for the account locale). Features degrade gracefully when a capability is missing.
Quick start (Docker)
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 (TOTP codes are supported via the "two-factor code" field, which Stalwart accepts as password$code).
Development
Requirements: Node ≥ 20.10 (22 recommended), npm ≥ 10.
npm install
# against a real Stalwart (set STALWART_URL in .env or the environment)
npm run dev # server on :8080 (tsx watch) + Vite dev server on :5173 (proxying /api)
# against the built-in mock Stalwart ([email protected] / demo) — no real mailbox needed
npm run dev:mock # mock on :8788, server on :8080, Vite on :5173
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).
Configuration
All configuration is via environment variables (see .env.example):
| Variable | Default | Description |
|---|---|---|
STALWART_URL |
https://mail.example.com |
Base URL of Stalwart; the JMAP session is discovered at /.well-known/jmap |
APP_SECRET |
(required in production) | Secret used to derive session encryption keys |
PORT / HOST |
8080 / 0.0.0.0 |
Listen address |
TRUST_PROXY |
1 |
Honour X-Forwarded-* from a reverse proxy |
SECURE_COOKIES |
auto |
auto (Secure on https), 1, or 0 for plain-HTTP dev |
SESSION_TTL / SESSION_REMEMBER_TTL |
43200 / 2592000 |
Idle session lifetime (seconds), with/without "keep me signed in" |
SESSION_FILE |
(unset) | Persist sessions across restarts (ciphertext only) |
IMAGE_PROXY |
1 |
Route remote images through the privacy proxy |
MAX_UPLOAD_BYTES |
52428800 |
Upload size limit (Stalwart has its own limit too) |
APP_NAME |
ihasmail |
Branding |
Keyboard shortcuts
Press ? anywhere. Highlights: c compose · / search · j/k navigate · o/Enter open · u back · e archive · # delete · ! spam · s star · r/a/f reply/reply-all/forward · v move · l label · x select · ⇧I/⇧U read/unread · g i inbox · g l calendar · g c contacts · Ctrl+Enter send.
Known issues / pending QA
Verified against the mock server and, for the core mail flows, against a live Stalwart 0.15.5. Still pending live verification:
- HTML signatures — Stalwart caps identity signatures at 2 KB. ihasmail compacts pasted HTML, moves images to Files and, if still too large, keeps the full signature in Files behind a short marker (other clients see a text fallback). The end-to-end flow (save → compose → send with inline logo) is implemented but not yet confirmed on the live server.
- Files — the live server runs an older Stalwart build than
main;FileNode/querythere rejectsisTopLevel/parentIdfilters, so ihasmail falls back to listing all nodes and building the tree client-side. Upload/rename/move/delete still need a live pass. - Self-service credentials — the 0.15.x REST path is confirmed live against Stalwart 0.15.5 (2026-08-24): password change, app passwords, and enabling and disabling 2FA, on a real mailbox. The 0.16 registry path has only been exercised against the mock, which enforces the same rules a real server does (current password required, password policy, a TOTP code on every request once 2FA is on, app passwords exempt from it) — it still wants a pass against a real 0.16 server. Password changes are refused by Stalwart for accounts backed by an external directory (LDAP/SQL/OIDC); the server's own message is shown when that happens.
- Recurring events: colour/category/edit/delete apply to the whole series (per-occurrence overrides aren't supported by the server yet).
- Editable date boxes are always Gregorian and in Latin digits, even for locales whose display uses another calendar or numbering system (
fa-IR,th-TH,ar-EG) — they keep the locale's field order and separator, but a Buddhist-era year in a text box does not round-trip against the Gregorian calendar grid. Non-Gregorian calendar support is not implemented. - The account locale is read from
x:AccountSettings/get, whose permission the built-in user role has, falling back tox:Account/get(which needs the admin-onlysysAccountGet). Both are Stalwart 0.16 methods: on older servers neither is reachable — they do not implement the registry and reject a request that so much as names theurn:stalwart:jmapcapability — so there the locale still falls back to the browser's and can be chosen by hand.
Roadmap / not yet
- Snooze and scheduled send (needs server-side support)
- Read-receipt (MDN) sending, S/MIME / OpenPGP
- Translations (strings are English-only for now)
License
Copyright (C) 2026 LINUXexpert.org
ihasmail is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See LICENSE for the full text.








