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:
The most common problems¶
- New mail does not appear until I reload
- Everyone is signed out after every deploy
- "Invalid username or password", for an account with two-factor sign-in
- Some people are signed out at random
- A scheduled message went out immediately
- Nobody is listed when I try to share
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:
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:
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:
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:
- Fetch
<STALWART_URL>/.well-known/jmapas that user. - Look for the
urn:stalwart:jmapcapability inprimaryAccountsand in each account'saccountCapabilities. - If it is there, something between ihasmail and Stalwart is changing the response. Fix that.
- 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.
- Does the account use two-factor sign-in? Create an app password in Stalwart's own settings, and sign in to ihasmail with that.
- 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:
-
Test from inside the container, because that is what has to reach Stalwart:
-
Check
STALWART_URL: scheme and host, no path. - Check that DNS works inside the container.
- If Stalwart is on a private address, check the Docker network can reach it.
- 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, leaveIMMUTABLEunset and putSESSION_FILEon a volume. - Without
IMMUTABLE: make sureSESSION_FILEpoints at a volume. The image defaults it to/data/sessions.json, whichdocker-compose.ymlputs on a named volume. - Did
APP_SECRETchange? 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:
- Make sure your reverse proxy forwards
X-Forwarded-Proto, and that ihasmail trusts it — see Every visitor looks like the same address. - 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.
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:
- On a version before
2026.8.31+pr144, upgrade. - 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:
MAX_UPLOAD_BYTESin ihasmail — 50 MiB by default.- Your reverse proxy's body limit —
client_max_body_size 60min the example config. - 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:
- Allow images from that sender. Remote images are blocked until you do.
- 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:
- Check your role on Stalwart manages accounts or domains.
- If it was granted while you were signed in, sign out and in again, or wait up to half an hour.
- 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:
- The page says how many accounts still use the domain. Move or delete them first.
- 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/healthor Settings › About - the Stalwart generation that Settings › About reports
docker logsfrom 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.