Configuring ihasmail¶
Everything is an environment variable. There is no configuration file to manage, no admin UI, and nothing to migrate between versions — which is a consequence of ihasmail keeping no state of its own rather than a stylistic choice.
The server reads a .env beside the app if there is one, but a real
environment variable always wins: the loader only fills in names that are not
already set. That is what makes docker run --env-file and a committed
.env.example coexist safely.
Pointing at Stalwart¶
| Variable | Default | Notes |
|---|---|---|
STALWART_URL |
https://mail.example.com |
Scheme and host, no path. The JMAP session is discovered at <STALWART_URL>/.well-known/jmap; a trailing slash is trimmed for you |
UPSTREAM_TIMEOUT |
30000 |
Milliseconds to wait on any one upstream call |
Only the ihasmail server talks to Stalwart. Browsers never do, which is why there is no CORS to arrange and no reason for Stalwart to be reachable from outside your network unless you want it to be for other clients.
Sessions and cookies¶
ihasmail never puts credentials in the browser. It authenticates against
Stalwart, then keeps the upstream credentials server-side, sealed with a key
derived from APP_SECRET — plaintext passwords are never written anywhere.
| Variable | Default | Notes |
|---|---|---|
APP_SECRET |
(required in production) | Session encryption keys are derived from it. openssl rand -base64 48 |
SESSION_TTL |
43200 |
Idle lifetime in seconds — 12 hours |
SESSION_REMEMBER_TTL |
2592000 |
Idle lifetime with keep me signed in — 30 days |
SESSION_FILE |
(unset) | Persist sessions across restarts. The Docker image sets /data/sessions.json, on the named volume |
SECURE_COOKIES |
auto |
auto marks cookies Secure when the request arrived over HTTPS; 1 or 0 to force |
COOKIE_NAME |
ihm_session |
Rename if two instances share a hostname |
LOGIN_RATE_LIMIT |
10 |
Failed sign-ins allowed per 15-minute sliding window, counted per IP and per IP + username |
Only ciphertext reaches SESSION_FILE. Losing it signs everyone out; leaking
it gives up nothing without APP_SECRET. Changing APP_SECRET invalidates
every existing session, which is the fastest way to sign the world out.
Behind a reverse proxy¶
| Variable | Default | Notes |
|---|---|---|
HOST / PORT |
0.0.0.0 / 8080 |
Listen address |
TRUST_PROXY |
1 |
Honour X-Forwarded-* — but only from a peer in TRUSTED_PROXIES |
TRUSTED_PROXIES |
(loopback + private ranges) | Comma-separated CIDRs or addresses whose forwarding headers are believed |
The default covers the ordinary case — a proxy on the same host or the same Docker network — without naming anything. A request from outside those ranges is attributed to its socket address whatever its headers claim, because otherwise anyone could pick their own key for the login rate limiter and never be limited.
Do not set TRUSTED_PROXIES to 0.0.0.0/0
That tells ihasmail to believe every client about who it is. The rate limiter, and every log line naming an address, become fiction.
Uploads, images and branding¶
| Variable | Default | Notes |
|---|---|---|
MAX_UPLOAD_BYTES |
52428800 |
50 MiB. Stalwart has its own limit; the lower of the two wins, and your proxy needs a body limit above both |
IMAGE_PROXY |
1 |
Route remote images in messages through the privacy proxy, Gmail-style. 0 loads them from the sender's server directly |
APP_NAME |
ihasmail |
Branding, shown in the UI and by /api/health |
SOURCE_URL |
the upstream repo | Where this instance's source can be had |
STATIC_DIR |
web/dist |
Where the built SPA is served from. The image sets /app/web/dist |
SOURCE_URL is a licence obligation, not decoration
ihasmail is AGPL-3.0-or-later. Section 13 asks whoever runs a modified
version over a network to offer its users that version's source — not the
one it was forked from. If you have patched it, point SOURCE_URL at your
own tree: 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.
Remote images are blocked by default regardless, with a per-sender allow-list
in the UI. IMAGE_PROXY decides what happens when someone allows them: through
your server (the sender learns nothing about the reader) or directly (the
sender learns the reader's IP address, user agent and the moment they opened
it). The proxy is SSRF-safe — it will not be talked into fetching your internal
network.
Settings that are not here¶
User-facing preferences are not server configuration. Identity, signatures,
locale, date format, theme, labels, templates and the rest live in a
settings.json in the account's own JMAP Files, on Stalwart — so they follow
the account between browsers and devices, including private windows, and they
are backed up with the mail store. ihasmail stores nothing itself.
Settings that describe this screen rather than the account deliberately stay in the browser: pane sizes, density, font size, sidebar state, and the notification toggles, which track a permission the browser grants per device.
Stalwart-side settings some features need¶
Three features depend on the server being configured a particular way. None of them can be fixed from ihasmail's side.
Scheduled send needs futureRelease¶
This one is worth reading twice, because the failure is silent.
Stalwart advertises the delay in the account's
urn:ietf:params:jmap:submission capability — maxDelayedSend: 2592000 and
FUTURERELEASE among its submissionExtensions. But the MTA only honours a
hold when futureRelease is set under the session's MTA extensions, and
that setting defaults to false. With it off, Stalwart accepts the
HOLDUNTIL parameter, skips the hold, and sends the message immediately
without an error — while the capability still claims thirty days.
So set futureRelease to the longest hold you want to allow before relying on
scheduled send. Shorter than 30 days is fine: a request past the limit is
refused honestly, naming it. The capability is no evidence either way. Only a
submission tells you.
The Scheduled folder is ihasmail's, not Stalwart's
A held message would otherwise land in Sent the moment the submission is created, so ihasmail files it in Scheduled and reconciles the folder on the way in — released messages to Sent, cancelled ones back to Drafts. Nothing in Stalwart does that moving, so if ihasmail is never opened again the message still goes out; it is only the folder that waits to be tidied.
Requires Stalwart 0.16.17 or newer for three fixes it depends on,
including HOLDUNTIL accepting RFC 3339 date-times again.
Web Push needs VAPID¶
Notifications that arrive with ihasmail closed are Web Push signed with VAPID
(RFC 9749), which Chromium and Safari require. ihasmail offers it only
when Stalwart advertises urn:ietf:params:jmap:webpush-vapid with a real
applicationServerKey; where the capability is absent the feature is hidden
rather than failing.
Stalwart pushes straight to the browser's own push service — no relay, no extra
service to run, and ihasmail's server is not in the delivery path. With
emailpush also advertised, the payload carries sender and
subject, so the notification is useful without a round-trip, and the filter
deciding what is worth notifying about stays on the server, so spam never
leaves it.
\"Closed\" means ihasmail, not the browser
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 closed, a notification arrives immediately. With the browser fully quit and continue running background apps off, nothing arrives until it is started again — and a push message carries a TTL, so one that expires first is dropped rather than delivered late. Installing ihasmail as a PWA does not change this on a desktop.
Self-service credentials and the account locale¶
Settings › Security — password change, app passwords, two-factor — is built on
Stalwart's x:AccountPassword and x:AppPassword registry objects, which are
0.16 and later only. Password changes are refused by Stalwart for accounts
backed by an external directory (LDAP, SQL, OIDC); ihasmail shows the
server's own message when that happens.
The account's locale is read from x:AccountSettings/get, whose permission the
built-in user role carries. It falls back to x:Account/get, which needs the
admin-only sysAccountGet, and then to the browser's locale — so a user
picking their own date format by hand is the expected outcome on a server that
will not answer, not a fault.
Next: Using ihasmail — the parts of the app that are not self-evident.