-
Notifications
You must be signed in to change notification settings - Fork 0
Feature Accounts
English · Tiếng Việt · 中文
Since 0.1.0: multi-account IMAP, plus Gmail and Outlook by OAuth. This is the most basic thing SkimMail does — read several mailboxes from different providers in one unified inbox — and this page is its reference.
A self-hoster's mail is rarely in one place: a personal Gmail, a work Outlook, a couple of IMAP mailboxes on a domain you run yourself. SkimMail connects to each independently, over each account's own network path if you give it one (see Connections), and keeps a local, read-first cache of headers (and optionally bodies — see Message body cache) so the unified inbox is fast without needing every provider to support the same protocol extensions.
Running this for more than one person? Read Security's per-user access control section — Sync now let an operator act on another user's account in every release from 1.10.0 through 1.17.0, fixed in 1.18.0.
Settings ▸ Accounts. Listing accounts is available to every signed-in user (viewer and above); adding, editing, syncing, re-authenticating and testing a connection are operator actions; deleting an account is owner-only. See Users and roles for what each role means.
Host, port, security (tls / starttls / none), username (defaults to the
email address if left blank) and password, plus two optional per-account
overrides: an egress (which proxy, VPN or relay this account's traffic uses —
see Connections) and a sync depth in days.
Adding one is a real connection test, not just a form save. SkimMail dials the server and lists its mailboxes before the account is ever written to the database — through the assigned egress if you picked one, so a misconfigured proxy is caught immediately rather than surfacing later as a silent sync failure. A failed validation refuses to save the account at all.
Not sure which to pick? App password or OAuth? compares the two in plain terms. This page is the procedure once you have chosen.
There are two reasons to use OAuth rather than an app password, and only one of them is a preference:
- Outlook.com and Hotmail have no alternative. Microsoft does not issue app passwords for personal accounts and has switched basic authentication off permanently. For those mailboxes OAuth is the only way in.
- Gmail has an alternative, and it is simpler. An app password takes two clicks and no Cloud project. OAuth is worth the setup when a Workspace admin has blocked app passwords, or when you would rather not store a password that opens the whole Google account.
SkimMail ships no shared OAuth client of its own, and this is deliberate
rather than unfinished. https://mail.google.com/ is a restricted scope:
Google grants it only to applications that have passed an independent security
assessment (CASA Tier 2), repeated annually. An application that qualifies is
tied to whoever owns it, and a client secret published in an open-source
repository would be revoked once and break every installation at the same
moment. Thunderbird ships its own verified client and says in that same source
file: "Don't copy these values for your own application — register one for
yourself!" Roundcube and Nextcloud Mail take the same bring-your-own approach
SkimMail does.
So: register an application — see
Register the OAuth application for the
console-by-console steps at Google and Microsoft — then paste its client id
and secret into Settings ▸ About ▸ Setup health ▸ OAuth. Environment
variables
(GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET,
MICROSOFT_CLIENT_ID/MICROSOFT_CLIENT_SECRET) still work and still win over
the stored value, showing the field as locked and naming the variable to
remove. Without both values for a provider, that provider's button is refused
(oauth_not_configured) rather than shown broken.
The scopes SkimMail requests:
| Provider | Scopes |
|---|---|
https://mail.google.com/, userinfo.email, openid
|
|
| Microsoft |
https://outlook.office.com/IMAP.AccessAsUser.All, offline_access, openid, email, profile
|
Microsoft was connected for the first time on 2026-09-18 — recently, and not completely. A personal Outlook account was taken end to end: the sign-in completed, thirteen folders were listed and mail synced. Until that day nobody on this project had watched any of it work. Its first renewal has now been watched too, and it came through cleanly — which is the part worth reporting, because Microsoft replaces the stored sign-in every time it renews, and a mistake there ends the connection permanently rather than for an hour. What has NOT been watched: whether a Microsoft 365 work or school account behaves the same — that is a different registration, under an administrator who can block the app outright — and anything beyond the first few hours. Gmail was verified on 2026-09-17 and has been through a renewal. If you connect a Microsoft mailbox — successfully or not — please report what happened; these sentences only get shorter that way.
Both are the scopes their vendor documents for IMAP. SkimMail's sync engine
speaks IMAP, so a scope that grants a REST API — Gmail's gmail.modify, or
Microsoft Graph's Mail.ReadWrite — is not accepted by the mail server no
matter how valid the token looks. Do not add a Microsoft Graph scope
alongside the Outlook one: an access token carries a single audience, and
mixing them either fails outright or, worse, silently issues a token for
whichever resource was named first.
Register both of these redirect URIs on the same application. They cost one
line each in the provider's console, and the Setup health ▸ OAuth screen shows
both with a copy button — typing them by hand is the commonest cause of
redirect_uri_mismatch.
| Redirect URI | What it is for |
|---|---|
<BASE_URL>/api/oauth/callback |
The ordinary flow. The provider sends the browser straight back to SkimMail. Needs a public HTTPS address. |
http://localhost:8642 |
The manual flow. Nothing listens there. |
The ordinary flow is what most installations use: click Continue with
Google, approve, and you are returned to SkimMail. It requires BASE_URL to
be set to an address the provider can reach.
The manual flow exists for everything else — a box on a home or office network with no public address, no domain and no TLS. Click No public address? Connect by pasting the sign-in result instead and SkimMail shows you the sign-in URL with a Copy button beside it.
Copy it and open it in whichever browser is signed in to the account — that is the point of showing the URL rather than only a link. A headless server is usually being administered from some other machine, and often the session signed in to Google or Microsoft is on a laptop or a phone rather than on the box running SkimMail. Paste it there. (If the browser you are reading this in is the right one, the "open it in a new tab here" link beside the URL saves you the round trip.)
Approve access, and that browser will land on
http://localhost:8642/?code=…&state=… and fail to load. That failure is
the design, not a fault: nothing is listening on that port. Copy the whole
address out of the address bar and paste it back into SkimMail, which reads the
code out of it and finishes the exchange.
The line you paste is protected by two independent locks, and it is worth knowing which is which. Your client secret — the provider will not exchange that code for a token without it, and it never leaves your server. PKCE — a one-time value your server keeps to itself and the provider checks. The secret alone already makes a copied line useless to whoever sees it; PKCE means that remains true even if a provider handles PKCE poorly. The sign-in expires after fifteen minutes; start again if you take longer.
The seven-day trap. A Google consent screen left in publishing status Testing issues refresh tokens that Google revokes after seven days, for any scope beyond name, email and profile — which includes the mail scope. The account then stops syncing and has to be reconnected, every week. Move the app to In production to stop it. The cost is an "unverified app" warning on the consent screen and a cap of 100 users, both of which are fine for a self-hosted instance.
The unverified-app warning is normal. https://mail.google.com/ is a
restricted scope, so until the application passes verification the consent
screen warns about it. That is expected and is not a sign that anything is
misconfigured. It must not be "fixed" by requesting a narrower scope — a
narrower scope is what stops IMAP working at all.
A tunnel gives a LAN instance a real public HTTPS address, which turns it into
the ordinary case: Cloudflare Tunnel and Tailscale Funnel both do this without
opening a port. Then set BASE_URL to the address it gives you and use the
ordinary flow. For Gmail specifically, an app password remains the shortest
path and avoids all of the above.
Each field is patched independently — a request that omits a field leaves it unchanged, it is never reset to a default. What you can change after creation: display name, group membership (see Account groups), egress assignment, bind address, sync depth, and pausing or resuming that account's own sync (which also clears an auto-stop, the same as a successful re-auth does).
IMAP accounts have a Re-authenticate action that takes a new password, re-validates it the same way adding an account does, and — if the account's sync had been auto-stopped by repeated failures (see Sync and Sync Health) — resumes it immediately.
OAuth accounts have the same button, and it does the right thing. For a Gmail or Outlook account, Re-authenticate sends you back to the provider's consent screen rather than asking for a password. Coming back mints a fresh token and updates the account you already have — it is matched on provider and address, so you do not end up with a duplicate.
An OAuth account's detail panel shows when its access token expires and offers a Refresh button beside it. Access tokens last about an hour and are renewed automatically on the next connection, so a clock reading expired is ordinary and is drawn as plain text, not as a warning. Refresh is there for when you want to see the clock move now — or, more usefully, to find out whether the grant is still alive without waiting for a sync to fail.
One signal does mean act: a red Needs reconnect badge on the account. That appears when authentication itself has failed — the grant was revoked, expired, or rotated away — and only reconnecting will fix it. Retrying cannot. SkimMail raises it on the FIRST failed sign-in rather than after the retry limit, so the warning arrives while the mailbox is still working rather than after it has stopped.
If the alert channel for oauth_expiring is switched on (see
Notifications), the same event is delivered there.
Do not delete the account to fix an expired token. Removing it throws away its settings, its egress assignment and its cached mail for no benefit; the button above is the whole fix.
Every account gets its own sync_depth_days, and the number it starts with is
the instance default from Settings ▸ Sync ▸ Default sync depth (falling
back to the SYNC_DEPTH_DAYS environment variable, and to 30 days if neither
is set). Typing a number into the Add Account form overrides it for that
account, because the most specific answer wins.
Before 1.17.0 that default was ignored. Account creation hardcoded 30 days, so an operator who chose 90 — or full history — got an account that synced 30 days anyway, with nothing in the logs and the Settings screen still showing the number they had picked. If you set that field on an older version and wondered why nothing changed, that is why.
Changing the default does not move accounts you already have. It applies to accounts created after the change. An existing mailbox keeps the number on its own row, which is deliberate: a window you chose for one account should not be rewritten because the instance default moved later. To change one, edit that account.
Owner only, and it does one thing that matters beyond removing the row: the account's cached message bodies are condemned for cleanup inside the same transaction (see Message body cache), so a deleted mailbox does not go on counting against your storage meter forever. This removes SkimMail's local copy only — the mail itself is untouched on the provider, and re-adding the same account resyncs everything from IMAP, headers and all, from scratch.
The order accounts appear in (both the unified list and per-account view) is
a saved sort_order, changed by drag-and-drop and persisted with one call —
it is purely a display preference and has no effect on sync or egress.
| Available since | 0.1.0 (IMAP), OAuth added the same release cycle |
| Role | viewer can list; operator can add/edit/sync/reauth/test; owner deletes |
| Tier limits | Community 10 · Sponsor 25 · Pro unlimited — see Plan and license |
| Counted | per instance, not per user |
| Providers | any IMAP server; Gmail and Outlook via the operator's own OAuth app |
- It offers no shared/default OAuth application. Gmail and Outlook connect buttons stay disabled until the operator registers their own app with the provider — this is a real setup step, not a missing feature.
- Add-account validation checks reachability, not deliverability. SkimMail dials the server and lists mailboxes; it never sends a test message, because SkimMail is a read-first reader.
- Removing an account never touches mail on the provider. It deletes only what SkimMail cached locally.
- A changed sync depth is not retroactive in the sense of undoing what a narrower window already skipped — it changes the window the next sync uses going forward.
- Account groups — organizing accounts, and the settings a group can hand down to its members
- Connections — per-account egress (proxy, WireGuard, relay) and what happens when the assigned one is disabled or deleted
- Sync and Sync Health — auto-stop, the board, and the "Default sync depth" that new accounts start from
- Plan and license — the Community/Sponsor/Pro account limits
-
Configuration — the OAuth client env vars and
BASE_URL - Register the OAuth application — the console-by-console steps at Google and Microsoft
- Users and roles — what viewer/operator/owner can each do here
SkimMail · skimmail@base101.app · 2026-09-18 · commit f525934