-
Notifications
You must be signed in to change notification settings - Fork 0
Feature Connections
English · Tiếng Việt · 中文
Every account can leave your server by its own route. Since 1.0.0 SkimMail can send one mailbox's traffic through a SOCKS5 proxy, another's through an HTTP proxy, a third's through a WireGuard tunnel and a fourth's through a WebSocket relay you deploy yourself — and it will tell you the public address each one is actually observed arriving from.
Several mailboxes on one self-hosted server means one public IP address for all of them. A mail provider sees the same address signing in to several accounts, at the same times, with the same client. That correlation is the thing you cannot undo afterwards, and it is the reason this feature exists.
Assigning an egress does not prove it works, though — a proxy that silently stopped proxying looks exactly like one that works. So the second half of this page is about verification: the real dial behind the Test button, the measured egress IP, and the warning that fires when two routes turn out to arrive at one address.
Settings ▸ Connections & Proxy. The whole tab is operator level or above; installing the WireGuard engine plugin is owner-only.
An egress is assigned in two places:
- Settings ▸ Accounts ▸ pick an account ▸ Network egress — the account's own choice, plus an optional source IP.
- Settings ▸ Groups ▸ a group — inherited by every account in it that has not chosen its own.
| Kind | What you paste | Notes |
|---|---|---|
| SOCKS5 | socks5://user:pass@host:1080 |
socks5h:// is accepted too; username and password are optional |
| HTTP proxy | http://host:8080 |
https:// also accepted; standard CONNECT tunnelling |
| WireGuard | a wg-quick .conf, or a kernel interface name |
two modes, below |
| Relay | one or more wss:// base URLs plus a token |
a WebSocket→TCP relay you deploy |
WireGuard has two modes and they are not variations of one thing.
Embedded runs the tunnel inside the SkimMail process from a config you paste
or drag in — no root, no host networking — and needs the skimmail-egress-wg
plugin, which the Connections tab installs for you and reports on in the
"WireGuard engine" strip. OS interface uses a kernel interface you brought up
yourself with wg-quick; SkimMail does not manage it and only pins this
account's source IP to the address you give.
Relay tunnels IMAP over a WebSocket to a small service you deploy — either a Cloudflare Worker (the code is embedded in your build and shown in the tab, so it always matches the protocol version this server speaks) or the published container image run with a different command:
docker run -d --restart unless-stopped -p 8080:8080 \
-e RELAY_TOKEN=<your token> \
ghcr.io/lyquyduong/skimmail:latest skimmail relay
The relay refuses to start without RELAY_TOKEN, because an unauthenticated
relay is an open proxy. It only opens the ports in RELAY_ALLOW_PORTS, which
defaults to 993,143 — that allowlist, not the token, is what stops a leaked
token from becoming a usable open proxy for someone else. Endpoints must be
wss://; up to 8 are accepted and they are tried in order, so a dead primary
costs one retry instead of a degraded account.
Every secret on this tab — proxy URL, WireGuard config, relay token — is encrypted at rest under the instance master key and is write-only: the API reports only whether one is stored, never the value. Leaving a secret field blank on save keeps the stored one.
The resolution order is fixed and has exactly three steps:
- the account's own egress, if it has one;
- otherwise its group's egress;
- otherwise Direct — the server's own address.
The optional source IP (bind_addr) follows the same order, and an OS-mode
WireGuard row's own source IP wins over both.
Through it: the account's IMAP connection, its OAuth token refresh (so the provider sees one consistent source for the account), remote images fetched for that account's mail, and the egress-IP probe described below.
Not through it: notification and alert delivery, which goes direct because a webhook points at your own endpoint; and one-click unsubscribe, which leaves from the server's own address — see Unsubscribe shortlist. There is no egress for outgoing mail, because SkimMail does not send mail.
Test on a row opens a real connection through that egress. It is not a replay of the numbers already on the row, and that includes WireGuard: the tunnel is dialled through, not asked how it feels.
The target is the mail host of an account already routed through that egress —
the only host SkimMail knows you actually want reached. There is deliberately
no fallback to a well-known provider: opening a connection to
imap.gmail.com from your server, on behalf of someone who may not use Gmail
at all, would tell Google something you never agreed to share. With nothing
assigned, that half is skipped and you still get the egress IP.
A relay is the exception: its /healthz answers on its own and settles
both questions that matter — the relay is alive, and the protocol version it
speaks matches this build — so a relay is testable the moment you paste the
endpoint, and while it is switched off. A reachable relay running stale code is
reported as a failure with both version numbers named, because a mismatch
fails later anyway, obscurely, mid-sync.
The verdict is what fills the row's "last check ok / failed" stamp. A red result never switches a row off and never moves an account.
Pressing Test also sends one request to a public echo-IP service through that egress and reads back the address the service saw. Nothing is inferred from your configuration: an egress nobody has tested stays visibly unconfirmed rather than being reported as working. There is no timer and no probe on sync — the only trigger is a person pressing Test.
That echo service is a third party. It learns the egress address and that something asked; never your mailbox, your mail, or which account the egress belongs to. If you would rather it were your own service, set:
EGRESS_IP_PROBE_URL=https://echo.example.org/ip
Direct has a row of its own on this tab, marked baseline. It is the address every account you have not routed through an egress arrives from, and it is what every other row is read against: if a proxy's observed address equals this one, the proxy is not proxying.
Four things the address line can say, and they are not interchangeable:
- an address and when it was seen — a normal, measured result;
- an address marked one sample, for a relay — see the limits below;
- not probed yet — nobody has pressed Test;
- the deployed relay speaks protocol v1 — it predates the field entirely, so redeploy the relay from this build to see an address at all.
If two different enabled egresses — or an egress and Direct — are observed arriving from the same address, a warning appears above the rows naming them. Accounts split across those egresses are not separated at all: a provider sees one address for all of them. The usual cause is that one egress is not actually carrying traffic (a dead proxy, a tunnel that leaked, two endpoints sharing an exit node).
The same check runs over accounts in Settings ▸ Accounts, phrased in accounts rather than egresses.
Two things it deliberately never flags: accounts that share one egress on
purpose, and relays. A serverless relay hands out a new address on every
connection — measured: three probes, three different addresses in Cloudflare's
2a09:bac5::/32 — so comparing one would raise a false alarm after every
probe, and a warning that cries wolf on a schedule stops being read.
Changed in 1.8.0, and it is a breaking change worth reading twice.
Switching an egress off does not detour its accounts. They keep the assignment, they do not borrow their group's egress, and they do not go Direct — they stop syncing and go Degraded until you switch it back on or move them somewhere else. The same fail-closed rule applies to an egress whose secret cannot be decoded, and to all four kinds equally.
Before 1.8.0, switching a proxy off detached it from its accounts, which then kept syncing direct, from your own address, with no warning — and the assignment was gone for good. That silent fallback is what this replaced.
Deleting an egress is different from disabling it. The database detaches
every account and group that referenced it (ON DELETE SET NULL) and those
accounts go Direct on the next dial. If you want an account to sync without
an egress, that is a choice you should make deliberately by setting its egress
to Direct — not by deleting a row.
One consequence to know about: the account pickers only offer enabled egresses, so an account pointed at a switched-off one shows as Direct on its card in Settings ▸ Accounts while the server is in fact refusing to connect for it. The Connections tab is the screen that tells the truth here — it names how many accounts a switched-off egress is holding.
Each row carries a derived status — healthy / degraded / down / unknown — and it is derived from signals SkimMail already has rather than from a synthetic prober, so "down" means real traffic is failing.
Two signals are folded together, worst wins. The egress's own liveness: for
WireGuard, the age of the last handshake (fresher than 180 s is healthy, older
than 600 s or never is down); for a relay, a short /healthz probe, where a
version mismatch reads as degraded because it answers but will fail on real
traffic. And the account traffic routed through it: all of its accounts
failing is down, some failing is degraded, none failing is healthy.
An egress that has gone down while accounts are on it also raises a
proxy_down alert, at most once every 30 minutes per egress so one shared dead
route does not fire one alert per account. See
Notifications.
| Available since | 1.0.0 (relay 1.8.0, egress IP 1.8.1) |
| Role | operator; WireGuard engine install is owner |
| Community / Sponsor / Pro | 3 / 5 / unlimited configured connections |
| Relay endpoints | up to 8 per row, wss:// only, token required |
| Relay ports |
993,143 by default (RELAY_ALLOW_PORTS) |
| IP probe | on Test only, never on a timer |
- A serverless relay can never give you a static IP. If your provider needs an allowlisted address — Gmail context-aware access, Exchange conditional access — use a VPS with a fixed address behind SOCKS5 or WireGuard, or run the relay container on that VPS.
- A relay does not cost you IMAP IDLE. SkimMail refreshes IDLE about every 3 minutes and recycles the tunnel about every 25, because the platform drops an idle socket at ~270 s and ends sessions at ~30 minutes. Nothing to configure; noted so that a reconnect in the logs reads as routine.
- A relay endpoint is screened against internal addresses — private, loopback, link-local and cloud-metadata ranges are refused at save time and again at connect time, after DNS resolution.
-
Embedded WireGuard needs its plugin. Without
skimmail-egress-wgthe tunnel cannot be built and the accounts on it fail closed.
- It is not a VPN for the server. Only the traffic listed above is routed; updates, plugin downloads and webhook deliveries are not.
- It does not proxy the web interface. How you reach SkimMail is a different question — see Security for trusted proxies.
- It does not hide you from your mail provider. They still receive your credentials and know which mailbox you opened; what changes is the address they see and their ability to tie several accounts together.
- It does not measure bandwidth per egress. WireGuard rows show RX/TX from the tunnel itself; nothing else is metered.
- It does not probe on a schedule. An address on a row can be old. The timestamp beside it says how old.
-
Configuration — where
EGRESS_IP_PROBE_URLsits among the config planes - Sync and Sync Health — what "Degraded" means on an account and how auto-stop interacts with a dead egress
-
Notifications — the
proxy_downalert - Troubleshooting — symptoms when an egress is the cause
- Operations — the plugin that carries the embedded WireGuard engine
SkimMail · skimmail@base101.app · 2026-09-14 · commit 76610cb