Skip to content

Troubleshooting

Three things sit between a reader and their mail, and almost every problem belongs to exactly one of them:

browser ──(/api/*)──► ihasmail server ──(JMAP)──► Stalwart
 React SPA             Node + Hono                 the mail server

Two checks place most faults straight away:

curl -s http://127.0.0.1:8080/api/health     # is ihasmail itself up?
docker logs --tail 50 ihasmail               # what did it say when it failed?

If /api/health answers and the browser still cannot sign in, the fault is between ihasmail and Stalwart, not in the browser.

The server will not start

APP_SECRET must be set to a strong random value in production

Exactly what it says: NODE_ENV=production — which the Docker image sets — and APP_SECRET either unset or still change-me. Generate one with openssl rand -base64 48.

[ihasmail] APP_SECRET not set - using an ephemeral secret

A warning, not an error, and only outside production. Sessions are sealed with a key derived from that secret, so a new one on every restart means everybody is signed out on every restart.

Invalid integer for PORT: …

A numeric variable — PORT, SESSION_TTL, UPSTREAM_TIMEOUT, MAX_UPLOAD_BYTES, LOGIN_RATE_LIMIT — was given something that is not a number. Quotes and stray whitespace in a .env are the usual cause.

Nobody can sign in

"Your credentials are fine, but this mail server is older than Stalwart 0.16"

ihasmail decides this by looking for Stalwart's urn:stalwart:jmap capability, which Stalwart advertises per account — in primaryAccounts and each account's accountCapabilities — and never in the session-level capabilities. So a genuine 0.16 server is still refused if something between ihasmail and Stalwart rewrites or trims the session response. Fetch <STALWART_URL>/.well-known/jmap as that user and look for the capability in those two places before concluding the server is old.

If it really is 0.15, the last release that runs on it is tagged stalwart-0.15-support.

"This mail server does not accept two-factor codes from webmail"

The password and code are almost certainly fine. Stalwart accepts a TOTP code only through an OAuth flow, and offers no password grant, so no client holding a username and password can pass one. Create an app password in Stalwart's own settings and sign in with that; app passwords bypass TOTP.

This is not a regression — the concatenated password$code form ihasmail once claimed to send is not a route the server has, and appears never to have been.

"Invalid username or password"

An ordinary rejection from Stalwart, with no code in the two-factor field. Worth ruling out an account backed by an external directory (LDAP, SQL, OIDC) whose password lives elsewhere.

"Too many attempts. Please wait a few minutes and try again."

Ten failed sign-ins per fifteen-minute sliding window, counted per IP and per IP + username. It clears itself, and restarting the server clears it immediately — the counters are in memory.

If this fires for everyone at once, the more likely cause is everyone sharing one IP.

"Could not reach the mail server" / "The mail server did not respond in time"

ihasmail could not get to Stalwart. Test from inside the container, since that is what has to reach it:

docker exec -it ihasmail wget -qO- https://mail.example.com/.well-known/jmap

Check STALWART_URL (scheme and host, no path), DNS inside the container, and whether a private-network address is reachable from the Docker network. A slow but working server wants a larger UPSTREAM_TIMEOUT.

Signing in works, then does not stick

Back at the sign-in page immediately after signing in

The session cookie is being set Secure and the page is not on HTTPS. SECURE_COOKIES defaults to auto, which follows X-Forwarded-Proto — so this is normally a proxy that is not forwarding it, or is not trusted. For a deliberately plain-HTTP instance, set SECURE_COOKIES=0.

Everyone signed out after a restart or a redeploy

SESSION_FILE unset, or set to a path inside the container that is not on a volume. The image defaults it to /data/sessions.json, which docker-compose.yml puts on the named volume. Changing APP_SECRET also invalidates every session, by design — it is the fastest way to sign the world out on purpose.

Every visitor looks like the same address

Rate limiting keys on the client address, so behind a proxy that ihasmail does not trust, every visitor shares one key and ten failures anywhere lock out everyone.

TRUST_PROXY is on by default, but X-Forwarded-* is believed only from a peer in TRUSTED_PROXIES — unset, that means loopback and the private ranges, which covers a proxy on the same host or Docker network. A proxy somewhere else has to be named there.

Not 0.0.0.0/0

That tells ihasmail to believe every client about who it is, which hands the rate limiter to anyone who wants to bypass it.

New mail does not appear until I reload

Live updates ride a Server-Sent Events connection, and a reverse proxy that buffers responses holds the events instead of passing them on. This is the single most common proxy mistake in front of ihasmail.

proxy_buffering off;
proxy_read_timeout 3600s;
reverse_proxy 127.0.0.1:8080 {
    flush_interval -1
}

Without proxy_read_timeout the connection is cut after nginx's default 60 seconds; it reconnects, so updates arrive in bursts a minute apart rather than never — which is the shape of the symptom when only that line is missing.

Notifications

No Web Push option in Settings at all

Stalwart is not advertising urn:ietf:params:jmap:webpush-vapid with an applicationServerKey. ihasmail hides the feature rather than offering something that cannot work. See Web Push needs VAPID.

Nothing arrives while ihasmail is closed

Expected, if the browser is also closed. Web Push is delivered over a connection the browser holds, so something of it has to be running — with the browser open and every ihasmail tab shut, notifications arrive immediately. With it fully quit and continue running background apps off, they queue until it starts again, and a push message that outlives its TTL is dropped rather than delivered late. Installing ihasmail as a PWA does not change this on a desktop.

Notifications keep arriving for an account nobody is signed into

They should not: the subscription is torn down on sign-out, because it belongs to the account rather than the session. If it survives, the sign-out did not complete — closing the tab is not signing out.

Sending

A scheduled message went out immediately

Stalwart's futureRelease is not set. It takes the HOLDUNTIL parameter, skips the hold and sends at once without an error, while the account capability still advertises thirty days. The capability is no evidence either way; only a submission tells you. See Scheduled send needs futureRelease.

A message is still in Scheduled after it was sent

The folder is ihasmail's own, and nothing in Stalwart moves messages out of it. It reconciles the next time ihasmail is opened — released messages to Sent, cancelled ones back to Drafts. The message itself went out on time regardless.

No option to send a read receipt

Offered only when the sender asked for one, and never for bulk mail, mailing lists or anything marked Auto-Submitted. If one was already sent, the $mdnsent keyword stops a second look from offering another.

An attachment is refused

Three limits stack, and the smallest wins: MAX_UPLOAD_BYTES (50 MiB by default), the reverse proxy's body limit — client_max_body_size 60m in the example config — and Stalwart's own. A 413 from ihasmail is the first; a 413 with no ihasmail JSON body is usually the proxy.

Mail looks wrong

Images do not load

Remote images are blocked until the reader allows that sender. With IMAGE_PROXY=1 (the default) an allowed image is fetched by your server, so it also fails if your server cannot reach the sender's host.

A message ignores the dark theme

Deliberate. Messages sit on a light card, untouched as the sender designed them. Appearance › Apply the theme to messages too changes that for plain-text mail and for HTML mail that brings no colours of its own; mail that styles itself is still left alone.

Settings and account

Changing a password is refused

Stalwart refuses password changes for accounts backed by an external directory (LDAP, SQL, OIDC). ihasmail shows the server's own message; the password has to change wherever the directory keeps it.

Settings did not follow me to another device

They live in the account's own JMAP Files, so they follow a sign-in. A device that already has ihasmail open keeps what it loaded until it signs in again, and conflicting writes are last-write-wins.

Some settings stay local on purpose and are meant not to follow: pane sizes, density, font size, sidebar state, and the notification toggles, which track a per-device browser permission.

There is no ihasmail folder in Files

Hidden on purpose, contents and all — it holds the settings file and signature images. Showing the folder is not the alternative to hiding it: the tree attaches a node whose parent is missing to the root, so hiding the folder alone would spill the signature images into the top level.

Editing one occurrence of a recurring event changes the series

Per-occurrence overrides are not supported by the server yet, so colour, category, edits and deletion apply to the whole series. ihasmail does not offer per-occurrence editing rather than pretending to.

Still stuck

Worth having to hand before opening an issue:

  • the version, from /api/health or Settings › About
  • what Settings › About reports as the detected Stalwart generation
  • docker logs around the failure
  • whether the same thing happens against the mock (npm run dev:mock), which rules Stalwart in or out

Issues go to the application repository.