-
Notifications
You must be signed in to change notification settings - Fork 0
Operations
English · Tiếng Việt · 中文
Running SkimMail after it is installed: keeping the data safe, seeing what it is doing, updating it, and understanding what it does on disk without being asked.
Every screen named here is one that actually exists in the shipped build. Where something has no screen yet, this page says so and gives the command instead.
One directory, DATA_DIR — /var/lib/skimmail for the apt package:
| Path | What it is |
|---|---|
skimmail.db |
The database: accounts, message headers, rules, settings |
blobs/ |
Cached message bodies (see Message body cache) |
master.key |
The key every stored account password is encrypted with |
plugins/ |
Runtime plugins, downloaded on demand |
Losing master.key is not recoverable. Every account password in the
database is encrypted with it, and from 1.11.0 so are the cached message
bodies. A database without its key is not a degraded instance — it is an
unreadable one. Any backup that is worth having contains both, and a backup of
skimmail.db alone is worth almost nothing.
The reverse is also true, and it is the part people get wrong when tidying up:
deleting master.key while keeping the database is the worst of the three
possible states. Remove all of DATA_DIR or none of it.
Settings ▸ Backup, owner only. The full guide — what an archive contains, the encryption and password rules, scheduled backups, restoring, and how to actually verify a restore before you need it — is its own page: Backup and restore.
The one fact worth repeating here because it explains the table above: an
archive carries master.key as its own entry inside it. That is exactly why
losing that file is not a "restore from backup" problem the way a missing
database row would be — a database without its key and an archive without its
key entry both fail the same way.
Settings ▸ Logs. The level (debug / info / warn / error) is a live
switch: changing it applies immediately, no restart needed, and it persists
over the boot default set by LOG_LEVEL (see Configuration).
Three ways to actually read the logs:
-
The in-app viewer, on the same screen — a realtime stream over a WebSocket, backfilled with the last ~400 buffered lines when you open it, filterable by level and free-text search, with pause/resume so a busy instance does not scroll a line out from under you before you can read it. No shell access to the host required.
-
A rotating file, when
LOG_FILEis set (the apt package sets it to/var/log/skimmail/skimmail.log). The path itself is env-only — nothing in the UI or the API can repoint it, so nothing running inside SkimMail can be tricked into writing an arbitrary file. What the UI can change at runtime: whether the file sink is on, the size it rotates at (LOG_MAX_SIZE_MB, 1–1024 MB, default 10), and how many rotated copies it keeps (LOG_MAX_BACKUPS, 0–20, default 3). A usage bar shows how full the current file is; Download streams it to your browser, and Truncate clears it after a confirm. -
The journal, always on regardless of the file sink:
sudo journalctl -u skimmail -fUse
sudo. Without it you will see an empty or partial journal rather than an error, which reads like "nothing is happening" when in fact you are not allowed to see it.
Format (text or json, LOG_FORMAT) is env-only and read-only in the UI —
level and rotation are the only knobs with a live switch.
Role note. The tab itself is reachable at operator and above, and
watching the live viewer is an operator action. Everything else here —
reading or changing the level and rotation settings, downloading the file, and
truncating it — is owner-only in the API
(GET/PUT /api/settings/logs, GET /api/logs/download,
POST /api/logs/truncate). Because the screen loads its settings before it
renders anything else, an operator who opens Settings ▸ Logs today gets a
screen that never finishes loading rather than a viewer with the settings card
hidden — worth knowing before you assign roles around this tab. See
Users and roles.
Settings ▸ Updates, owner only. SkimMail can check its own release feed
(GitHub Releases by default; override with UPDATE_FEED_URL for a
self-hosted or air-gapped mirror — see Configuration) and, on
some deployments, install the update itself.
Checking is opt-in and off by default — nothing calls out to GitHub until you turn it on or click Check now. Turn on automatic checking and it polls on an interval (default 24 h, editable) starting two minutes after boot, so a restart never causes a surprise phone-home. A release newer than what you are running shows its notes and a version badge; Skip records that version so it stops being announced.
Whether the button can install it depends on how you deployed:
| Deploy kind | What SkimMail reports | What the Update button does |
|---|---|---|
| Prebuilt binary tarball, unpacked into a directory the service user owns | self |
Downloads, verifies, swaps and restarts itself in place |
| Container (Docker / containerd / Kubernetes) | managed |
Refuses; pull and run the new image instead |
| apt package | managed |
Also refuses — the shipped systemd unit's ProtectSystem=strict makes /usr/bin read-only to the skimmail service user, so the binary cannot rewrite itself even though it is not in a container |
Detection is automatic (SkimMail looks for /.dockerenv and container
cgroups, then tries to create a temp file next to its own executable) — there
is no setting to override it. Since the apt path is managed too, update it
the ordinary way instead:
sudo apt update && sudo apt install --only-upgrade skimmail
"Unable to locate package" right after a release almost always means a stale
local index, not a failed publish. Run sudo apt update first.
When the button can run — deploy kind self, a release asset published
for your platform, and a build carrying the release verification key (every
official apt/Docker/tarball build does) — Update now downloads the release
asset plus SHA256SUMS and its .minisig signature; verifies the signature
against the fixed key baked into official builds; checks the asset's SHA256
against the verified checksum list; extracts the binary; renames the current
one aside to <binary>.old; renames the new one into place; and re-execs into
it — the process replaces itself, so there is no separate "restart" step. If
the swap succeeds but the re-exec itself fails (uncommon, platform-dependent),
the new binary is already installed and a manual service restart finishes the
job.
A binary built without the verification key stays notify-only. It will tell you an update exists and refuse to install it rather than install something it cannot check — the same key backs the plugin manifest signature described under Plugins, below.
Rolling back is manual, and only possible for a self deploy. The apply
step never deletes <binary>.old on success, so if a fresh version misbehaves
you can stop the service, move .old back over the current binary, and
restart. There is no button for this and no automatic detection of a bad
update — the decision and the recovery are both yours.
Install updates automatically (nested under the check toggle, and inert
unless that is also on) applies a self-updatable release the moment a check
finds one. On a managed deploy this switch has nothing to act on, because
the update it would trigger is refused the same way the button is; the UI
says so beside the toggle.
Settings ▸ Plugins, owner only. Egress and remote-access features ship as runtime plugins rather than being compiled into the base binary, so a mail-only install stays small. Three plugins exist today:
| Plugin | Kind | Backs |
|---|---|---|
cloudflared |
external (the official Cloudflare binary, pinned) | Remote access — Cloudflare Tunnel |
skimmail-tunnel-ts |
built by SkimMail | Remote access — Tailscale |
skimmail-egress-wg |
built by SkimMail | The embedded WireGuard engine on Connections |
They live under DATA_DIR/plugins/ (see "Where everything lives", above),
run as child processes with the server's own privileges, and are re-verified
every time they are spawned — not only at install.
Install pre-fetches the pinned artifact for your OS/architecture, SHA256-checks it, and shows a download progress bar; nothing is running yet, so there is no confirmation step. Update reconciles an installed plugin to whatever version the manifest currently pins — if it is backing something live (a running tunnel, an active embedded-WireGuard egress), a confirm names what drops briefly while it stops and respawns. Uninstall removes the files outright and names the same impact first. The embedded WireGuard engine also has an in-process "builtin" build used before the plugin exists on disk at all; it is neither installable nor removable from this screen, because there is no file to act on yet.
Reading the inventory is an operator-level route in the API, but the whole Settings ▸ Plugins tab requires owner in the UI, so in practice nobody below owner reaches this screen through the app at all.
The catalog of what is installable, and at what version, comes from
plugins.json, published next to the apt repository on the same GitHub Pages
origin as the self-update feed. On a build carrying the release
verification key (since 1.10.0, and true of every official build), the
manifest's own .minisig signature is checked before anything in it is
trusted, and a missing signature is refused outright — a SHA256 pin is only as
trustworthy as the document that carries it, and deleting a file must not be a
way to turn that check off. A build with no verification key (a custom build
with UPDATE_PUBKEY unset) still enforces the SHA256 pin per artifact, but
logs a one-time warning that the pin itself is unverified.
Settings ▸ Sync. Per-account state, the last error, and the consecutive failure count. An account that fails repeatedly is auto-stopped rather than retried for ever; the board is where you see that and re-enable it.
The stop is deliberate and other features respect it. Background body prefetch, for example, skips an auto-stopped account entirely and never records its own failures against that counter — optional background work must not be able to disable a mailbox that is otherwise healthy.
Full model — the three board states, the auto-stop threshold and its clamps, and which sync knobs need a restart to take effect — is its own page: Sync and Sync Health.
Cached message bodies now have a lifecycle. The full description is on Message body cache; operationally, three things happen without you asking:
One reconciliation pass on the first start of 1.11.0. It builds the index for bodies cached by earlier versions and, on filesystem storage, deletes the orphans they left behind. You will see it once:
reconciled body cache store=fs indexed=1843 bytes=284127744 extra=12 deleted=12
Exactly once. It records that it has run, so a restart does not repeat it.
On S3 it counts and stops. The log line ends needs_purge=true and nothing
is deleted, because that bucket may also hold your backup archives and SkimMail
will not guess. Deleting them is a decision you make:
Settings ▸ Security ▸ Message body cache ▸ Purge unaccounted, or the same thing over the API:
curl -sS -b cookies -X POST http://localhost:8080/api/settings/cache/purge
Owner only, both ways. The button arrived in 1.11.1; on 1.11.0 the API was the only route, because the panel shipped inside a Settings tab that is hidden.
Eviction, continuously. Above CACHE_MAX_SIZE_MB the least recently read
bodies go first; past BODY_TTL_DAYS they expire by age. The environment sets
the initial values (see Configuration); from 1.11.1 the
same two limits are editable in Settings ▸ Security, and a value saved
there wins over the environment from then on.
apt remove stops and unregisters the service and leaves DATA_DIR
untouched — including the master key. So does apt purge: there is no
postremove script, so nothing automated ever deletes your mail.
Erasing it is manual and irreversible:
sudo rm -rf /var/lib/skimmail
sudo userdel skimmail # optional: the service account
Reinstalling without doing that picks up exactly where you left off — nothing is re-downloaded.
Reference — Home · Configuration · Security · Troubleshooting · FAQ
SkimMail · skimmail@base101.app · 2026-09-15 · commit dffbb18