Skip to content

Troubleshooting

Find what you are seeing, do the fix, and open Why this happens only if you want the detail.

Start here

Run these two commands on the machine running ihasmail. They place most problems 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?
  • No answer from the first command? ihasmail is not running. Go to The server will not start.
  • It answers, but nobody can sign in? The problem is between ihasmail and Stalwart, not in the browser. Go to Nobody can sign in.
The three parts a problem can be in

Three things sit between a person 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

The most common problems

The server will not start

APP_SECRET must be set to a strong random value in production

Fix: set APP_SECRET to a long random value. Make one with:

openssl rand -base64 48
Why this happens

The Docker image sets NODE_ENV=production. In production, ihasmail refuses to start when APP_SECRET is unset or still says change-me.

[ihasmail] APP_SECRET not set - using an ephemeral secret

Fix: set APP_SECRET, as above. This is a warning, not an error.

Why this happens

It appears only outside production. ihasmail makes up a new secret each time it starts. Sessions are sealed with a key made from that secret, so a new secret on every restart signs everybody out on every restart.

Invalid integer for PORT: …

Fix: check your number settings for quotes or stray spaces. The same message can name PORT, SESSION_TTL, UPSTREAM_TIMEOUT, MAX_UPLOAD_BYTES or LOGIN_RATE_LIMIT.

Why this happens

A setting that must be a number was given something else. Quotes and stray whitespace in a .env file are the usual cause.

It exits saying IMMUTABLE is set, but SESSION_FILE is …

Fix: clear SESSION_FILE when you start the container:

-e IMMUTABLE=1 -e SESSION_FILE=

You do not need to edit your settings file: -e beats --env-file.

Why this happens

This is working as intended. IMMUTABLE=1 promises the instance keeps nothing durable, and a session file contradicts that. The image sets SESSION_FILE=/data/sessions.json by default, so running immutably means clearing it yourself.

It exits saying IMMUTABLE is set, but /app/ is writable

Fix: run the container read-only, with a small scratch area:

--read-only --tmpfs /tmp

In Docker Compose, that is read_only: true.

Why this happens

IMMUTABLE is set, but the container is not actually read-only. It is almost always a missing --read-only.

Why this is an error and not a warning: the alternative is silence. Saving sessions is best-effort. So an instance that claims to be immutable without being so looks completely healthy — until it is replaced and everybody is signed out for no visible reason. See Running immutably.

Nobody can sign in

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

Fix:

  1. Fetch <STALWART_URL>/.well-known/jmap as that user.
  2. Look for the urn:stalwart:jmap capability in primaryAccounts and in each account's accountCapabilities.
  3. If it is there, something between ihasmail and Stalwart is changing the response. Fix that.
  4. If the server really is 0.15, the last release that runs on it is tagged stalwart-0.15-support.
Why this happens

ihasmail decides the server's version by looking for Stalwart's urn:stalwart:jmap capability. Stalwart lists it per account, 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. Check both places before concluding the server is old.

"Invalid username or password"

Fix: if the password is right, check two things first.

  1. Does the account use two-factor sign-in? Create an app password in Stalwart's own settings, and sign in to ihasmail with that.
  2. Is the account backed by an external directory (LDAP, SQL, OIDC)? Its password lives in that directory, not in Stalwart.
Why this happens

It is an ordinary rejection from Stalwart. An account with two-factor sign-in cannot use its ordinary password here, and there is no code field to make up the difference. Stalwart takes a two-factor (TOTP) code only through an OAuth flow, and offers no password grant. So an app holding a username and password has nowhere to send a code. App passwords bypass TOTP. The rejection looks like any other wrong password, which is the one unhelpful part.

The API can still answer \"does not accept two-factor codes\"

POST /api/auth/login accepts a totp field. It answers totp_unsupported when one is sent and upstream refuses. The sign-in page never sends it, so this reaches only something calling the API directly. The combined password$code form ihasmail once claimed to send is not a route Stalwart has, and appears never to have been.

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

Fix: wait. It clears itself. Restarting ihasmail clears it at once.

If it happens to everyone at once, the real cause is probably that everyone looks like the same address.

Why this happens

ihasmail allows ten failed sign-ins in any fifteen minutes. It counts them per IP address and per IP address plus username. The counts are kept in memory, which is why a restart clears them.

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

Fix:

  1. Test from inside the container, because that is what has to reach Stalwart:

    docker exec -it ihasmail wget -qO- https://mail.example.com/.well-known/jmap
    
  2. Check STALWART_URL: scheme and host, no path.

  3. Check that DNS works inside the container.
  4. If Stalwart is on a private address, check the Docker network can reach it.
  5. If Stalwart is slow but working, raise UPSTREAM_TIMEOUT.
Why this happens

ihasmail could not get to Stalwart at all, or gave up waiting.

Signing in works, then does not stick

Everyone is signed out after every deploy

Fix: it depends on how you run ihasmail.

  • With IMMUTABLE=1: this is expected, and no setting avoids it. If you would rather sessions survived, leave IMMUTABLE unset and put SESSION_FILE on a volume.
  • Without IMMUTABLE: make sure SESSION_FILE points at a volume. The image defaults it to /data/sessions.json, which docker-compose.yml puts on a named volume.
  • Did APP_SECRET change? That signs everyone out too.
Why this happens

With IMMUTABLE=1, sessions are held in memory because there is nowhere else for them to live. Replacing the container ends them. That is the one cost of running immutably: the trade, not a bug.

Without it, sessions are lost on restart when SESSION_FILE is unset, or set to a path inside the container that is not on a volume. Changing APP_SECRET invalidates every session by design — it is the fastest way to sign everyone out on purpose.

Signed out after a few minutes of not using it

Fix: on a computer that is actually yours, tick This is my own device when you sign in.

Why this happens

It is expected on a device signed in without that box ticked, which is the default. Leaving it unticked asks for a five-minute idle sign-out.

Back at the sign-in page straight after signing in

Fix:

  1. Make sure your reverse proxy forwards X-Forwarded-Proto, and that ihasmail trusts it — see Every visitor looks like the same address.
  2. If the instance is deliberately plain HTTP, set SECURE_COOKIES=0.
Why this happens

The session cookie is being marked Secure, and the page is not on HTTPS, so the browser will not send it back. 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.

Some people are signed out at random, others are fine

Fix: if you run more than one ihasmail behind a load balancer, turn on session affinity (sticky sessions) there, using the balancer's own cookie. Running more than one ihasmail shows the setup, and what else changes when you scale out.

To confirm it, send the same session twice in a row. Different answers mean no affinity:

# The same session, twice in a row. Different answers means no affinity.
curl -s -o /dev/null -w '%{http_code}\n' -b 'ihm_session=<cookie>' https://mail.example.com/api/auth/session
curl -s -o /dev/null -w '%{http_code}\n' -b 'ihm_session=<cookie>' https://mail.example.com/api/auth/session

Alternating 200 and 401 is the answer. ihm_session is the default cookie name; use yours if you set COOKIE_NAME.

Only one instance? Then it is not this

Look at Everyone is signed out after every deploy instead. With a single instance, random sign-outs are the idle timeout, a Secure cookie on a plain-HTTP page, or a restart with no SESSION_FILE.

Why this happens

This is almost always more than one ihasmail behind a load balancer without session affinity. Check it before anything else if you have recently scaled out. Nothing in the logs says "affinity": the failures look like ordinary expired sessions.

The session cookie is an id into the memory of the instance that issued it. An instance that did not issue it has nothing to look up, and answers 401. So a request sent to the wrong one signs that person out. Every request is balanced afresh, so signing in again just moves them to another instance: the symptom is a loop, not a one-off.

The pattern of complaints points at it too. Affinity failures hit some people constantly and leave others alone for hours. An expiry or an APP_SECRET change signs out everyone at once.

It has to be the balancer's own cookie because the sign-in request that creates the session does not carry ihasmail's cookie yet. There is nothing to route on until the balancer supplies one.

Every visitor looks like the same address

Fix: if your reverse proxy is not on the same machine or Docker network as ihasmail, add its address to TRUSTED_PROXIES.

Do not set it to 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.

Why this happens

Sign-in rate limiting counts by the visitor's address. Behind a proxy that ihasmail does not trust, every visitor shares the proxy's address, so ten failures anywhere lock out everyone.

TRUST_PROXY is on by default, but X-Forwarded-* headers are 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 anywhere else has to be named.

New mail does not appear until I reload

Fix: stop your reverse proxy buffering responses. This is the 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
}

If new mail arrives in bursts about a minute apart, only proxy_read_timeout is missing.

Why this happens

Live updates travel over a Server-Sent Events connection that stays open. A proxy that buffers responses holds the events instead of passing them on.

Without proxy_read_timeout, nginx cuts the connection after its default 60 seconds. ihasmail reconnects, so updates arrive in bursts a minute apart rather than never.

Notifications

No Web Push option in Settings at all

Fix: turn on Web Push in Stalwart. See Web Push needs VAPID.

Why this happens

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

Nothing arrives while ihasmail is closed

Fix: keep the browser running. Closing every ihasmail tab is fine; quitting the browser is not, unless it keeps running in the background.

Why this happens

It is expected if the browser is also closed. Web Push is delivered over a connection the browser holds, so something of the browser has to be running. With the browser open and every ihasmail tab shut, notifications arrive immediately.

With the browser fully quit and continue running background apps off, they wait until it starts again. A push message that outlives its TTL (its time to live) is dropped rather than delivered late. Installing ihasmail as an app does not change this on a desktop.

Background notifications will not turn on

Fix: sign out, and sign in again with This is my own device ticked.

Why this happens

A notification subscription outlives the tab and belongs to the account. On a device not marked as your own it is refused, because it would go on delivering mail to that machine long after you had left it.

Notifications worked, then stopped after a week or so

Fix: open ihasmail. Notifications resume on their own, and nothing needs turning off and on again. Opening it about once a week keeps them going.

On a version before 2026.8.31+pr144, upgrade.

Why this happens

A push subscription can expire: seven days is the most a JMAP server may grant. Only a running page can renew it, because registering is an authenticated request and the service worker has no session to make one with. ihasmail renews on every start. Left shut for longer than a week, notifications stop until you next open it.

Versions before 2026.8.31+pr144 never renewed at all. Background notifications lapsed a week after being switched on, and stayed off until the switch was toggled by hand.

The switch says background notifications are on, but none arrive

Fix:

  1. On a version before 2026.8.31+pr144, upgrade.
  2. On a current version, the subscription's verification is not completing — see below.
Why this happens

Before 2026.8.31+pr144, the switch answered "does this account have a subscription". So it showed as on the moment any other device had one. A phone that had never registered, or whose registration had lapsed, looked correct and delivered nothing. It now checks this device.

On a current version, the server sends a code over the push channel, and the device has to send it back. Until it does, the subscription is registered but silent.

Notifications keep arriving for an account nobody is signed in to

Fix: sign in on that device and sign out properly. Closing the tab is not signing out.

Why this happens

The subscription is removed on sign-out, because it belongs to the account rather than the session. If it survives, the sign-out did not complete.

Sending

A scheduled message went out immediately

Fix: set futureRelease in Stalwart. See Scheduled send needs futureRelease.

Why this happens

With futureRelease not set, Stalwart takes the HOLDUNTIL parameter, skips the hold and sends at once, without an error. Meanwhile the account capability still advertises thirty days. So the capability is no evidence either way; only a submission tells you.

A message is still in Scheduled after it was sent

Fix: nothing to fix. Open ihasmail and the folder tidies itself. The message went out on time.

Why this happens

The Scheduled folder is ihasmail's own, and nothing in Stalwart moves messages out of it. It is tidied the next time ihasmail is opened: released messages go to Sent, canceled ones back to Drafts.

No option to send a read receipt

Fix: nothing to fix, if one of these applies:

  • the sender did not ask for a receipt;
  • the message is bulk mail, from a mailing list, or marked Auto-Submitted;
  • a receipt was already sent.
Why this happens

ihasmail offers a receipt only when the sender asked for one. Once one is sent, the $mdnsent keyword stops a second look from offering another.

An attachment is refused

Fix: raise the smallest of three limits:

  1. MAX_UPLOAD_BYTES in ihasmail — 50 MiB by default.
  2. Your reverse proxy's body limit — client_max_body_size 60m in the example config.
  3. Stalwart's own limit.

A 413 error from ihasmail is the first. A 413 with no ihasmail JSON body is usually the proxy.

Why this happens

The three limits stack, and the smallest one wins.

Mail looks wrong

Images do not load

Fix:

  1. Allow images from that sender. Remote images are blocked until you do.
  2. If they still do not load, check that your server can reach the sender's image host.
Why this happens

With IMAGE_PROXY=1 (the default), an allowed image is fetched by your server rather than your browser. So it also fails if your server cannot reach the sender's host.

A message ignores the dark theme

Fix: turn on Appearance › Apply the theme to messages too.

Why this happens

It is deliberate. Messages sit on a light card, as the sender designed them. The setting changes that for plain-text mail, and for HTML mail that brings no colors of its own. Mail that styles itself is still left alone.

Settings and account

Changing a password is refused

Fix: change the password wherever your directory keeps it — LDAP, SQL or OIDC — not in ihasmail.

Why this happens

Stalwart refuses password changes for accounts backed by an external directory. ihasmail shows the server's own message.

Settings did not follow me to another device

Fix: sign out and in again on the other device.

Some settings are meant to stay on each device: pane sizes, density, font size, sidebar state, and the notification toggles.

Why this happens

Settings 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. If two devices write at once, the last write wins.

The notification toggles stay local because they track a browser permission that is granted per device.

There is no ihasmail folder in Files

Fix: nothing to fix. It is hidden on purpose, contents and all. It holds the settings file and signature images.

Why this happens

Hiding only the folder would not work. The file tree attaches anything whose parent is missing to the top level, so the signature images would spill out there.

Administration

Administration is grayed out in the account menu

Fix: sign out, and sign in again with This is my own device ticked.

Why this happens

Administration is only available on a device you have marked as your own.

Administration is not in the account menu at all

Fix:

  1. Check your role on Stalwart manages accounts or domains.
  2. If it was granted while you were signed in, sign out and in again, or wait up to half an hour.
  3. Otherwise, the installation may run with ADMINISTRATION=0.

An account opens read-only in Administration

Fix: ask someone whose role can do at least as much as that account.

Why this happens

The account has permissions your role does not. Stalwart does not check a password change, role change or deletion against your role, so ihasmail does: it will not change an account that can do more than yours.

Remove domain is unavailable

Fix:

  1. The page says how many accounts still use the domain. Move or delete them first.
  2. If it says the DKIM keys have to go first, your role cannot remove them. Someone whose role can will have to remove the domain.

Calendar

Editing one occurrence of a recurring event changes the series

Fix: none yet. Color, category, edits and deletion apply to the whole series.

Why this happens

The server does not support changing a single occurrence yet. ihasmail does not offer it rather than pretending to.

Sharing

I shared something and the other person cannot see it

Fix:

  • A calendar, address book or folder of files: ask them to look under Available to add, not Shared with me, and add it.
  • A mail folder: this cannot be fixed from ihasmail. Use Stop sharing to clear an old share.
Why this happens

Nothing shared is used until the person it was shared with adds it.

Mail folders are different. Stalwart accepts the share, stores it, and never delivers it. ihasmail no longer offers to share mail folders for that reason. One shared before that change still offers Stop sharing, so it can be cleared.

Nobody is listed when I try to share

Fix: set allowDirectoryQueries in Stalwart's Sharing settings.

Only where accounts should see each other

Enable it only where every account belongs to the same organization, or tenants are cleanly separated. It lets any signed-in account look up the others.

Why this happens

The people picker asks the server's directory, and Stalwart keeps directory lookups switched off by default.

Anything already shared is listed and can be removed whether or not the directory answers. So an empty picker never traps an existing share.

A shared address book will not stay added

Fix: add it again. It should now stay added. Nothing is needed from the person who shared it.

Why this happens

Adding a book normally writes to the owner's copy. Stalwart refuses that for a book shared read-only: "You are not allowed to modify this address book". ihasmail asks the server first, and when the server says no it remembers the book in your own settings instead. So it stays added, and follows you between devices like the rest of your settings.

Contacts from a shared address book do not come up when I write a message

Fix: add the address book first, from Available to add.

Why this happens

Until it is added, a shared book suggests nothing in the To field. That is deliberate: a book you were handed but never asked for cannot put strangers in front of you mid-sentence.

I am offered an account that never shared anything with me

Fix: ignore it, or add it if you want it. Nothing is used until you add it.

Why this happens

Its calendar or address book appears under Available to add. Stalwart reports every collection in an account you can reach, with full rights, whether or not anyone meant to share it. Nothing in its answer separates "shared with me" from "reachable", which is why ihasmail waits to be told.

I cannot share with more than ten people

Fix: raise maxShares in Stalwart's settings.

Why this happens

maxShares caps how many accounts one item can be shared with. It defaults to 10.

Still stuck

Have these ready before opening an issue:

  • the version, from /api/health or Settings › About
  • the Stalwart generation that Settings › About reports
  • docker logs from around the failure
  • whether the same thing happens against the mock (npm run dev:mock), which tells you whether Stalwart is involved

Issues go to the application repository.