`BASE_PATH=/mail` mounts the whole app under a prefix, for a host that is not
ihasmail's alone. Unset -- every deployment that exists -- is the domain root
and is byte-for-byte what it was: the canonical form of the setting is the
empty string, and `""` concatenated onto `/api/health` is `/api/health`.
That choice of canonical form is the whole design. A trailing slash would have
been the obvious alternative, and it fails quietly in exactly one place: at the
root it makes `//api/health`, which is not a path on this host but a
protocol-relative URL to a host called `api`. One call site forgetting to
branch is a request leaving the origin. So the empty string, one leading slash,
no trailing one, worked out once in `scripts/basePath.mjs` -- plain JS, next to
`version.mjs`, because the web build and the server both have to reach the same
answer and two implementations of "what does /mail/ mean" is precisely the bug
where the server serves an app whose script tags point somewhere else.
`/mail`, `mail`, `/mail/` and `//mail//` all mean the same mount; a deployment
should not fail over a trailing slash.
Unlike everything else ihasmail is told, this one cannot wait for the process
to start. The bundle writes its own asset URLs into index.html, so `BASE_PATH`
is read at build time for Vite's `base` as well as at run time for the routes,
and the Dockerfile carries one value into both. Get them out of step and the
page comes up blank with a 404 in a console nobody has open -- so the static
handler, which is reading index.html anyway, checks what it asks for and says
so in the log once per build.
Everything moves together. The API mounts at `${base}/api`; the router is
given the base once, so every `<Route path>` and `<Link href>` stays written
root-absolute and wouter does the rest; `apiFetch` adds the prefix in one place
rather than at forty call sites; the session cookie's Path narrows to the mount
so two instances on one host cannot sign each other out.
Two things need no prefix at all, and it is worth saying why they were not
given one. A manifest's members resolve against the manifest's own address, so
relative URLs there follow the mount with nothing substituted at build time --
which is also why `public/` needed no template step. The service worker is the
same trick: it is served from the mount, so `new URL("./", self.location)`
tells it where that is, and a worker that derives the value cannot disagree
with the page that registered it.
Anything outside the mount is a 404 rather than the app shell, and
`stripBasePath` does not use `startsWith` -- under `/mail` this process shares
a hostname, and answering `/mailbox` with our index would shadow a neighbour
instead of letting it 404 honestly. For the same reason the notification-click
handler now checks the path as well as the origin: `includeUncontrolled` widens
`matchAll` to the whole origin, which off the root would have navigated a
stranger's tab to our inbox.
Inline images in a draft were the one silent trap. They are matched by their
blob URL on the way out, once unanchored and once anchored, and a bare
`/api/blob/` still appears inside `/mail/api/blob/...` -- so one pattern would
have replaced the tail and left `/mail` in front of a `cid:`, and the other
would have missed and sent the message linking to the sender's own webmail.
Both patterns are built from the base now.
72 lines
3.0 KiB
Bash
72 lines
3.0 KiB
Bash
# ---- ihasmail server configuration ----
|
|
|
|
# Base URL of your Stalwart server (scheme + host, no path). ihasmail discovers
|
|
# the JMAP session at <STALWART_URL>/.well-known/jmap.
|
|
STALWART_URL=https://mail.example.com
|
|
|
|
# Random secret used to derive encryption keys for persisted sessions.
|
|
# Generate with: openssl rand -base64 48
|
|
APP_SECRET=change-me
|
|
|
|
# Listen address
|
|
HOST=0.0.0.0
|
|
PORT=8080
|
|
|
|
# Serve the app from a subpath instead of the domain root, for a reverse proxy
|
|
# that maps https://example.com/mail/ here. Leave it unset for the root, which
|
|
# is what every deployment gets unless it asks otherwise. "/mail", "mail" and
|
|
# "/mail/" all mean the same thing.
|
|
#
|
|
# The prefix must reach ihasmail intact -- do not strip it in the proxy -- and
|
|
# it has to be set for the *build* as well as the run: the web bundle writes
|
|
# its own asset URLs, so a build that does not know the prefix produces an app
|
|
# that cannot load itself under one. With Docker that means
|
|
# `--build-arg BASE_PATH=/mail` alongside `-e BASE_PATH=/mail`.
|
|
# BASE_PATH=/mail
|
|
|
|
# Set to "1" when running behind a TLS-terminating reverse proxy (trusts
|
|
# X-Forwarded-* and marks cookies Secure). Set to "0" for plain-HTTP dev.
|
|
TRUST_PROXY=1
|
|
# Peers whose X-Forwarded-* headers are believed. Unset means loopback and the
|
|
# private ranges, which covers a reverse proxy on the same host or Docker
|
|
# network. A request from anywhere else is attributed to its socket address,
|
|
# whatever the headers claim -- otherwise anyone could pick their own key for
|
|
# the login rate limiter.
|
|
# TRUSTED_PROXIES=10.0.0.0/8,192.168.1.5
|
|
SECURE_COOKIES=auto
|
|
|
|
# Session lifetime (idle timeout) in seconds. "Remember me" extends to SESSION_REMEMBER_TTL.
|
|
SESSION_TTL=43200
|
|
SESSION_REMEMBER_TTL=2592000
|
|
|
|
# Where to persist sessions so restarts don't log everyone out (optional).
|
|
# Leave it empty to hold sessions in memory only, which is what an immutable
|
|
# instance does -- see IMMUTABLE below.
|
|
SESSION_FILE=./data/sessions.json
|
|
|
|
# Assert that this instance is running as an immutable container: read-only
|
|
# root filesystem, no durable state of its own. It is checked rather than
|
|
# taken on trust -- the server refuses to start if SESSION_FILE is set, or if
|
|
# the filesystem it is installed on turns out to be writable. Off by default.
|
|
# Running one looks like:
|
|
# docker run --read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE= ...
|
|
# The cost today is that a restart signs everyone out, since there is nowhere
|
|
# left to keep the sessions. Removing that cost is what the OAuth work is for.
|
|
# IMMUTABLE=1
|
|
|
|
# Upstream timeouts / limits
|
|
UPSTREAM_TIMEOUT=30000
|
|
MAX_UPLOAD_BYTES=52428800
|
|
|
|
# Remote-image privacy proxy (Gmail-style). Set to 0 to load remote images directly.
|
|
IMAGE_PROXY=1
|
|
|
|
# Branding
|
|
APP_NAME=ihasmail
|
|
|
|
# Where this instance's source can be had. ihasmail is AGPL-3.0-or-later, which
|
|
# asks whoever runs a modified version to offer *that* version's source -- so if
|
|
# you have patched it, point this at your own tree. Shown on the sign-in page
|
|
# and in Settings > About.
|
|
SOURCE_URL=https://github.com/Coffey-Labs/ihasmail
|