Ratatoskr

Ratatoskr

Docker app from Magnus Pihl's Repository

Overview

Ratatoskr is a self-hosted webmail client for any IMAP/SMTP account. One container, configured by environment variables, with data in SQLite under /data. Features: unified inbox, conversations, Web Push notifications and PWA, snooze, and dark mode. Every email is treated as hostile: server-side sanitising, sandboxed rendering, and remote images are proxied. REQUIRES HTTPS through a reverse proxy (SWAG, Nginx Proxy Manager, Traefik, Caddy) for push notifications and installing it as an app. WebSockets on /api/ws must be proxied. Set the Base URL to the public https address and keep Trust Proxy enabled.

Ratatoskr

A simple, good-looking, self-hosted webmail client for any IMAP/SMTP account.

  • Your mail server stays the source of truth. Ratatoskr keeps users, their encrypted account settings and preferences (plus, for notifications, push subscriptions and the newest INBOX UID already notified about, and for snoozed mail, folder, UID and wake-up time) in one SQLite file. To be quick, it also keeps an encrypted, disposable copy of each account's newest INBOX and Sent mail in a second one; see Mail cache.
  • One container. Configured by environment variables, with a volume for the SQLite files. No Redis, no Postgres, no sidecars.
  • Modern UI. Clean three-pane layout, first-class dark mode, usable on a phone.
  • Every email is treated as hostile. See Security model.

Features

  • Several IMAP/SMTP accounts per user (host, port, TLS/STARTTLS, credentials, separate SMTP settings, display name, colour). Connections are tested when you save. Passwords are encrypted at rest (AES-256-GCM) and never sent back to the browser.
  • Login with a username and password, with any OpenID Connect provider (Authentik, Keycloak, …), or both side by side; see Login. Each user owns their own accounts, with strict per-user isolation.
  • Unified inbox across all accounts (merged by date, paginated with per-account cursors), plus per-account folder trees with unread counts. Live updates via IMAP IDLE pushed over one WebSocket per browser session.
  • Message list with sender, subject, snippet, date, unread/flagged/answered/attachment indicators, and an account colour tag in the unified view.
  • Conversations, Gmail-style: a message and its replies are one row ("Ada, me 3") and are read together, older messages folded. Your replies are found in the Sent folder, also when you sent them from another mail client. Messages belong together when they share Message-IDs (References / In-Reply-To) and their subjects match apart from "Re:" and the like. Actions on a conversation apply to all its messages in the folder; the copies in Sent are left alone.
  • Reading plain text and HTML, with inline (cid:) images and attachment downloads. Remote images are blocked by default; "Load images" has the server fetch them and embed them in the message, so the sender never sees your IP address. "Always load from example.com" adds the sender's domain (and its subdomains) to a per-user list whose mail loads images automatically; manage the list in Settings.
  • Contact pictures instead of initials, from Gravatar, Libravatar (including a domain's own federated server) or, for senders at a company rather than a free mail provider, the icon of their website. Like remote images, the server fetches them, so these services never see your IP address or when you read a message. Turn them off in Settings.
  • Mark read/unread, flag, move, archive, delete (to Trash, expunge only from Trash), multi-select. On touch screens, swipe a message left or right to delete, archive, mark read/unread or flag it (which action each direction does is set in Settings).
  • "Unread only" filter for the current view. Messages you read there stay on screen until you change the view or refresh, so the list doesn't jump under your finger.
  • Snooze: hide a message until a date and time you pick (or Later today / Tomorrow / This weekend / Next week). See Snooze.
  • Compose, reply, reply-all, and forward (with the original attachments). Includes a light rich-text editor, an automatic plain-text alternative, attachments, drafts (saved to the account's Drafts folder), a copy in Sent, and recipient autocomplete from recently used addresses. Special-use folders (\Sent, \Drafts, \Trash, \Archive, \Junk) are detected with name fallbacks.
  • mailto: links open a new message in Ratatoskr: in plain-text mail directly, in HTML mail in a new tab (clicks inside the sandboxed message frame can't be caught). Settings → "Open email links with Ratatoskr" makes the browser send mailto: links on any website here too; the installed app (PWA) registers for them on its own where the browser supports it. Only recipients, subject and a plain-text body are taken from a link, and nothing is sent until you press Send.
  • Server-side IMAP search in the current folder, or across all inboxes in the unified view.
  • Dark mode that follows the system, with a manual override. Simple HTML and plain-text mail get dark styling. Heavily designed mail (newsletters) is shown on a light "paper" card instead of being force-inverted, with a per-message toggle.
  • Notifications for new mail on phones and desktops (Web Push), and an unread badge on the app icon. Install Ratatoskr as an app (PWA) from the browser; see Notifications.
  • Keyboard shortcuts: j/k to move, r reply, a reply all, f forward, e archive, b snooze, # delete, s flag, u mark unread, c compose, Ctrl/Cmd+Enter to send.

Quick start (Docker)

  1. Register an OIDC client at your identity provider (see OIDC setup). To use username/password login only, skip this and drop the OIDC_* lines in step 3.

  2. Create a .env next to docker-compose.yml:

    RATATOSKR_SECRET_KEY=$(openssl rand -base64 48)
    OIDC_CLIENT_SECRET=the-client-secret-from-your-provider
    
  3. Adjust RATATOSKR_BASE_URL and OIDC_* in docker-compose.yml, then run:

    docker compose up -d
    
  4. Put a TLS-terminating reverse proxy in front (see below), open https://mail.example.com, sign in, and add your first mail account.

Images are published to ghcr.io/<owner>/ratatoskr when a release is made (see Releasing), tagged with the version (1.2.3, 1.2, 1 from 1.0.0 on) and latest. You can also build it yourself with docker build -t ratatoskr ..

The server runs as user and group 1000:1000 and takes over the data directory on start (it chowns it when anything in it belongs to someone else). To run as someone else, set PUID and PGID, e.g. 99 and 100 (nobody:users) for an unRAID appdata folder. UMASK (e.g. 002) sets the permissions of the files it creates. Starting the container with --user (user: in Compose) also works; then it runs as that user, ignores PUID/PGID and changes no ownership.

Configuration

Variable Required Default Description
RATATOSKR_BASE_URL yes (prod) http://localhost:8080 Public URL of the app. Used for the OIDC redirect URI, cookie Secure flag and WebSocket origin check.
RATATOSKR_SECRET_KEY yes – Master secret, at least 32 characters (openssl rand -base64 48). Subkeys for credential encryption, session cookies and URL signing are derived from it with HKDF. If you change it, stored account passwords can no longer be decrypted.
RATATOSKR_PASSWORD_LOGIN no true without OIDC, false with OIDC Username/password login.
RATATOSKR_REGISTRATION no first Who may create a password account: open (anyone), first (only the first user of an empty instance) or closed.
OIDC_ISSUER_URL for OIDC – Issuer URL; /.well-known/openid-configuration is discovered from it.
OIDC_CLIENT_ID for OIDC – Client ID.
OIDC_CLIENT_SECRET usually – Client secret (confidential client).
OIDC_SCOPES no openid email profile Requested scopes.
OIDC_LABEL no Single sign-on Provider name on the login button ("Sign in with …"), e.g. Authentik.
RATATOSKR_TRUST_PROXY no false Trust X-Forwarded-* headers. Set to true behind a reverse proxy.
RATATOSKR_DATA_DIR no ./data (/data in the image) Where ratatoskr.db and the mail cache cache.db (SQLite) live.
PUID / PGID no 1000 / 1000 Docker image only: user and group the server runs as and that owns the data directory.
UMASK no – Docker image only: umask for files the server creates, e.g. 022 or 002.
PORT / HOST no 8080 / 0.0.0.0 Listen address.
RATATOSKR_BLOCK_PRIVATE_MAIL_HOSTS no false Refuse IMAP/SMTP hosts that resolve to private/internal addresses. Turn on if untrusted users can log in. Leave off if your mail server is on your LAN.
RATATOSKR_IMAP_IDLE_TIMEOUT no 300 Seconds before an unused pooled IMAP connection is closed.
RATATOSKR_MAX_MESSAGE_MB no 50 Largest message the server downloads for display.
RATATOSKR_MAX_UPLOAD_MB no 25 Largest total attachment upload when composing.
RATATOSKR_CACHE no true Keep a local copy of each account's newest INBOX and Sent mail, see Mail cache. false asks the mail server every time.
RATATOSKR_CACHE_MESSAGES no 2000 Newest messages kept per cached folder (50–100000). Older mail is fetched from the mail server as you scroll.
RATATOSKR_CACHE_MAX_BODY_MB no 1 Largest message whose content is kept (0: keep no message content).
RATATOSKR_CACHE_ACCOUNT_MB no 200 Message content kept per account; the least recently read goes first.
RATATOSKR_VAPID_SUBJECT no RATATOSKR_BASE_URL if it is https://, else mailto:postmaster@<host> Contact sent to push services (Apple, Google, Mozilla) with every notification: a mailto: or https: URL.
RATATOSKR_DEFAULT_* no – Starting values for new mail accounts, see Default account settings.
LOG_LEVEL no info fatal, error, warn, info, debug, trace.
RATATOSKR_DEV_LOGIN no false Development only: log in as a local dev user without a password or OIDC. Ignored when NODE_ENV=production (always set in the image).
RATATOSKR_DEV_ALLOWED_HOST_SUFFIX no – Development only: also serve the dev server on any host ending in this suffix (see Development). Ignored when NODE_ENV=production and by the production build.

Default account settings

If your users all get their mail from the same provider, set the server details once and the Add account form starts out filled in. Users can still change every field.

Variable Description
RATATOSKR_DEFAULT_IMAP_HOST / RATATOSKR_DEFAULT_SMTP_HOST Incoming and outgoing server.
RATATOSKR_DEFAULT_IMAP_SECURITY / RATATOSKR_DEFAULT_SMTP_SECURITY tls (default), starttls or none.
RATATOSKR_DEFAULT_IMAP_PORT / RATATOSKR_DEFAULT_SMTP_PORT Defaults to the standard port for the security mode (993/143, 465/587).
RATATOSKR_DEFAULT_MAIL_USERNAME Username for IMAP and SMTP.
RATATOSKR_DEFAULT_EMAIL Email address of the account.
RATATOSKR_DEFAULT_DISPLAY_NAME Name shown to recipients.
RATATOSKR_DEFAULT_ACCOUNT_LABEL Account label in the sidebar.
RATATOSKR_DEFAULT_MAIL_PASSWORD Optional shared password for IMAP and SMTP.

All of them except the password can contain tokens that are filled in for each user:

  • {username}: the login username of password users; for OIDC users the provider's preferred_username (or, without one, the part of their email address before the @).
  • {email}: the user's email address from login.
  • {name}: the user's name from login.

If a user has no value for a token (an OIDC user without an email address, say), the field starts empty instead. A misspelled token stops the server from starting.

RATATOSKR_DEFAULT_IMAP_HOST=imap.example.com
RATATOSKR_DEFAULT_SMTP_HOST=smtp.example.com
RATATOSKR_DEFAULT_MAIL_USERNAME={username}@example.com
RATATOSKR_DEFAULT_EMAIL={username}@example.com
RATATOSKR_DEFAULT_DISPLAY_NAME={name}

The default password is taken literally and never sent to the browser. When a user leaves the password blank, the server fills it in, but only for the default server: if the user points the account at a different host, they have to enter that host's password themselves.

Notifications

Ratatoskr is an installable web app (PWA) and can send notifications for new mail through the browsers' standard Web Push services. No native app, no Firebase or Apple developer account and no extra configuration are needed. The push keys (VAPID) are derived from RATATOSKR_SECRET_KEY. HTTPS is required.

Each user turns notifications on per device in Settings → Notifications:

  • Android (Chrome, Firefox, Edge, Samsung Internet) and desktop browsers: turn the switch on. Installing the app ("Add to Home screen" / "Install app") is optional.
  • iPhone and iPad (iOS/iPadOS 16.4 or newer): open the site in Safari, tap Share → Add to Home Screen, open Ratatoskr from the Home Screen, then turn the switch on. A Safari tab can't receive notifications.

What you get:

  • One notification per new unread message in any account's INBOX (several messages that arrive together are summarised in one). Tapping it opens the message.
  • Nothing while the app is open and focused on any of your devices, since the list updates live anyway.
  • Users choose whether notifications show sender and subject or only "New message" and the account name. The payload is end-to-end encrypted to the device (RFC 8291), but it does pass through Apple's, Google's or Mozilla's push service.
  • Notifications close themselves on all your devices once their messages are read or moved out of the inbox, in Ratatoskr or in any other mail client (within the 24 hours a notification is kept, and not across a server restart). Not on iPhone and iPad: Apple revokes notifications for sites that send pushes which show nothing.
  • The app icon shows the number of unread messages across all inboxes, where the platform supports badges.
  • On Android, the notification header shows the browser's icon and name unless the browser installs the app as a real Android app (a "WebAPK", as Chrome does). Browsers such as Vivaldi add a home screen shortcut instead, so their notifications are attributed to the browser; the squirrel still appears in the status bar and as the notification's picture.

How it works: while a user has notifications on at least one device, the server keeps one IMAP IDLE connection per account open on INBOX, even when no browser is open. Mind your provider's connection limits (Gmail allows 15 simultaneous IMAP connections per account). The server remembers the newest INBOX UID it has notified about, so after a restart it reports mail that arrived while it was down, but never old mail when notifications are switched on. Subscriptions that the push service reports as expired are removed automatically. Signing out stops notifications on that device.

Snooze

Snoozing moves a message to the account's Snoozed folder until the time you pick. Ratatoskr uses the folder the server marks as Snoozed (the IMAP \Snoozed attribute from RFC 9979), or a folder named "Snoozed", and creates one if there is none. IMAP has no way to store when a message should come back, so Ratatoskr keeps the wake-up time in its database and runs the timer itself.

When the time comes, the message goes back to INBOX as unread, with the current time as its arrival date, so it is at the top of the list. Messages larger than RATATOSKR_MAX_MESSAGE_MB are moved back as they are and keep their old date. Then:

  • your devices with notifications on get a "Back from snooze" notification (same content setting as for new mail; nothing while the app is open and in front of you);
  • open browser windows show a notice with a link to the message.

In the Snoozed folder each message shows when it comes back. Open one to Unsnooze it (back to the inbox right away) or snooze it again to change the time. A snooze can be undone from the notice that confirms it. Snoozes that came due while the server was down are handled when it starts. A message you move out of the Snoozed folder yourself, in Ratatoskr or another mail app, stays where you put it.

Login

Two login methods are available, alone or together:

  • Username and password. On by default when no OIDC provider is configured. With OIDC, turn it on with RATATOSKR_PASSWORD_LOGIN=true. Usernames are case-insensitive (an email address works); passwords need at least 10 characters and are stored as scrypt hashes. By default only the first person to open a fresh instance can create an account (RATATOSKR_REGISTRATION=first). To let others sign up, set RATATOSKR_REGISTRATION=open (for a while, or permanently on a private network), then back to closed. With open registration on a public instance, also set RATATOSKR_BLOCK_PRIVATE_MAIL_HOSTS=true so strangers can't point IMAP/SMTP at your LAN. After 10 wrong passwords for one username, or 30 from one IP address, logins are refused for 15 minutes. Users change their password in Settings.
  • OpenID Connect (Authentik, Keycloak, …): see below.

Password users and OIDC users are separate accounts, even if they share an email address.

Sessions are encrypted cookies valid for 14 days. They can't be revoked one by one: changing a password doesn't sign out other devices. Changing RATATOSKR_SECRET_KEY signs everyone out (and makes stored mail passwords unreadable).

OIDC setup

Create an OAuth2/OpenID Connect application (confidential client, authorization code flow) with:

  • Redirect URI: https://mail.example.com/auth/callback
  • Post-logout redirect URI (optional): https://mail.example.com/
  • Scopes: openid email profile

Authentik: create an OAuth2/OpenID Provider and an Application. The issuer URL looks like https://auth.example.com/application/o/<application-slug>/. Use Authentik's application bindings/policies to decide who may log in.

Keycloak: create a client with Client authentication on and Standard flow enabled. The issuer URL looks like https://auth.example.com/realms/<realm>.

Anyone your provider lets through can use Ratatoskr. Users are identified by issuer and sub claim, and each user sees only the mail accounts they added themselves. Ratatoskr uses PKCE, state and nonce, and supports RP-initiated logout if the provider advertises an end_session_endpoint.

Reverse proxy notes

  • Terminate TLS at the proxy and set RATATOSKR_BASE_URL to the public https:// URL and RATATOSKR_TRUST_PROXY=true.

  • WebSockets must be proxied on /api/ws (live updates). Nginx:

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;  # map $http_upgrade, see nginx docs
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 1h;
        client_max_body_size 30m;   # attachments
    }
    

    Caddy and Traefik proxy WebSockets automatically.

  • Allow request bodies at least as large as RATATOSKR_MAX_UPLOAD_MB (plus a little overhead).

  • Serve Ratatoskr on its own (sub)domain, not under a path prefix.

Mail cache

Asking the mail server for everything on every click is slow: one page of a folder takes several IMAP round trips, and finding the rest of each conversation in the Sent folder means header searches that some servers are slow at. So Ratatoskr keeps a copy of the newest messages (RATATOSKR_CACHE_MESSAGES, 2000 by default) of each account's INBOX and Sent folder: sender, subject, snippet, flags and the headers that tie conversations together. It also keeps the content of messages you have opened, and loads the newest unread ones ahead, up to RATATOSKR_CACHE_MAX_BODY_MB per message and RATATOSKR_CACHE_ACCOUNT_MB per account. The limits are server settings; users can't change them.

The mail server stays the source of truth:

  • Every action (read, flag, move, delete, send) goes to the mail server first; the copy is updated once the server has done it.
  • The copy follows the server. While INBOX is watched (a browser is open, or notifications are on), every change reported by IDLE is fetched right away, so the inbox shows without asking the server. Otherwise, and for Sent, a listing first gives the server a moment (1.5 s) to report changes, and shows the copy if it is slower; the list updates by itself when the changes arrive. Refresh always checks with the server first. Changes are fetched incrementally: new messages, changed flags (only those since the last check where the server supports CONDSTORE) and messages that are gone. A new UIDVALIDITY starts the folder over.
  • Other folders, searches, scrolling past the copy, attachments and folder counts always go to the mail server.
  • The copy is disposable. It is a separate file (cache.db), and anything wrong with it (a damaged file, an entry that doesn't decrypt, another secret key, a new format) just empties it, or the folder concerned, and it is filled again from the server. Deleting the file while Ratatoskr is stopped is always safe.

What is stored and how: subjects, addresses, snippets, Message-IDs, References and message content are encrypted with AES-256-GCM (a key derived from RATATOSKR_SECRET_KEY), each entry bound to its user, account, folder and message, so a copied row doesn't decrypt elsewhere. Conversations are matched through keyed hashes (HMAC) of the Message-IDs, not the IDs themselves. Readable in the file are only what queries need: folder names, UIDs, arrival times, sizes and the read/flagged/answered/ draft flags. An account's copy is deleted with the account, or when it is pointed at another server or login.

Security model

Every message is assumed to be hostile.

  • HTML mail is sanitized on the server with sanitize-html and a postcss-based CSS sanitizer. Removed: scripts, event handlers, forms, frames, objects, SVG/MathML, meta/base/link, comments, javascript:/data: links, CSS expressions, @import, behaviours, and anything hidden behind CSS escapes. Links are limited to http/https/mailto/tel and open in a new tab with noopener noreferrer. mailto: links are pointed at the app's own composer (/?mailto=…) just before display.
  • The result is rendered in an <iframe sandbox> without allow-scripts and allow-same-origin. That gives it an opaque origin, so it can't reach the app, its cookies or storage. A strict Content-Security-Policy inside the frame (default-src 'none'; img-src data:) blocks scripts, forms, frames and every network request: the rendered message never contacts any server, which also means it works behind cookie-based auth proxies.
  • Remote content is blocked by default. "Load images" makes the server download the images (<img>, background= and CSS url()) and embed them as data: URIs, so senders never see the reader's IP address, cookies or referrer. The fetcher only fetches http(s) on ports 80/443. It resolves DNS itself and refuses loopback/private/link-local/CGNAT/ULA/multicast/reserved addresses, including IPv4-mapped IPv6 and IP literals. It connects to the address it checked (no DNS-rebinding window), re-checks every redirect, and only accepts raster image types (no SVG). Limits per message: 6 parallel fetches, 10 MB per image, 15 MB and 100 images in total, and a 20 s budget. Anything over a limit, failing or blocked is left out, keeping its size and alt text as a placeholder. Fetched images sit in a small in-memory cache (per user, 10 minutes, 64 MB) so toggling or reopening a message doesn't download them again.
  • Sender-domain allowlist. The server decides when it fetches a message: if the From domain is on the user's list (exact match or a subdomain; case-insensitive, IDN normalised to punycode), images load automatically through the same fetcher. From is easy to forge, so as a cheap guard nothing auto-loads when the topmost Authentication-Results header (the one added by your own mail server) records dmarc=fail for the From domain; the user can still click "Load images". Without that header, or with pass/none, the allowlist applies as normal.
  • cid: images are inlined as data: URLs. Attachments are always served as downloads (Content-Disposition: attachment, application/octet-stream, nosniff, sandbox CSP).
  • The app sends a strict CSP (script-src 'self'), frame-ancestors 'none', nosniff, no-referrer, and other hardening headers.
  • Credentials: AES-256-GCM with a random IV, and the ciphertext bound to its user and account (AAD), so a copied database row doesn't decrypt elsewhere. Passwords are never returned by the API.
  • Cached mail is encrypted the same way; see Mail cache for what is readable.
  • Notifications: push subscription endpoints come from the browser, so the server treats them like remote image URLs. It accepts any HTTPS push service (so every browser's push service works), but only on the standard port, without credentials, IP literals or reserved names. It resolves DNS itself, refuses internal addresses, connects to the address it checked and never follows redirects. Payloads are encrypted for the receiving device. The service worker only shows notifications. It caches nothing and only opens paths of the app itself.
  • Sessions are encrypted, HttpOnly, SameSite=Lax cookies. State-changing API calls also require a custom header, which cross-site pages can't send. WebSocket upgrades check Origin.

Development

Requirements: Node.js ≥ 22.13 (uses the built-in node:sqlite) and Docker (for integration tests).

npm install
cp .env.example .env          # set RATATOSKR_SECRET_KEY; without OIDC you get password login (or use RATATOSKR_DEV_LOGIN=true)
PORT=5173 npm run dev         # Vite on $PORT, backend on $PORT+1 (proxied under /api and /auth)

In a git worktree (git worktree add), npm run dev also reads the main checkout's .env, so one file serves every worktree. A .env in the worktree overrides single values from it, and real environment variables override both. The database is not shared: each worktree keeps its own ./data (a relative RATATOSKR_DATA_DIR is relative to the worktree), because branches with different migrations would break each other's schema.

If you open the dev server through a reverse proxy, set RATATOSKR_BASE_URL to that public URL. Vite only accepts that host name (plus localhost).

If the same setup is reached through several proxied hostnames that differ only in a leading part (e.g. one per port: 5173--myworkspace--me.dev.example.com, 40140--myworkspace--me.dev.example.com), also set RATATOSKR_DEV_ALLOWED_HOST_SUFFIX=--myworkspace--me.dev.example.com. The suffix must start with . or -- and is matched exactly at the end of the host name, so lookalikes such as …dev.example.com.evil.test are refused. In development the WebSocket origin check then follows the request host, and dev login works on each of those hosts (cookies are per host, so log in once per host). OIDC still needs the real RATATOSKR_BASE_URL: its redirect URI is built from it, so OIDC logins always return to that one host.

Command What it does
npm run typecheck Type-check server and web client.
npm test Unit tests: sanitizer (hostile fixtures), MIME parsing, credential encryption, SSRF guards, unified-inbox merging, conversations, the mail cache, Web Push encryption (RFC 8291 test vector), notifications, snooze scheduling, the web client's pure logic and the release version script.
npm run test:integration Starts a throwaway GreenMail container (Docker), runs the full HTTP API against real IMAP/SMTP, and removes the container. Also builds the image's base stage and checks the container entrypoint (PUID/PGID, data directory ownership).
npm run build Compiles the server to dist/server and the web client to dist/web.
npm start Runs the production build.

Releasing

Pushes to main only run CI. To publish, open Actions → Release → Run workflow and pick a bump: patch, minor or major off the newest v* tag (the first release starts from 0.0.0). The workflow runs all tests, pushes the image tagged with the version and latest, and creates the vX.Y.Z tag and a GitHub Release with generated notes. For a specific version, push the tag yourself instead: git tag v1.2.3 && git push origin v1.2.3.

Architecture

server/src
  app.ts            Fastify setup, security headers, static files
  auth/             OIDC login (openid-client), password login (scrypt) and encrypted session cookies
  db.ts             SQLite store: users, encrypted accounts, preferences, recent recipients, image
                    allowlist, push subscriptions, snoozes
  cache/store.ts    Mail cache: encrypted copies of INBOX and Sent (a separate, disposable SQLite file)
  cache/sync.ts     Keeps the mail cache in step with the IMAP server
  imap/pool.ts      Lazy per-account IMAP connections with idle timeout; IDLE watchers
  imap/mail.ts      Folders, unified listing (k-way merge with per-account UID cursors), actions
  imap/related.ts   Finding the rest of a conversation (replies in Sent, older messages)
  mail/sanitize.ts  HTML sanitizer; mail/css.ts CSS sanitizer
  mail/compose.ts   MIME building (Nodemailer MailComposer); mail/send.ts SMTP + Sent/Drafts
  mail/snooze.ts    Snoozed folder, wake-up timer, putting mail back into INBOX
  net/              SSRF guards, the server-side remote image fetcher and contact pictures
  push/             Web Push (VAPID, RFC 8291 encryption) and new-mail notifications
shared/             Types, conversation grouping and the sandboxed iframe document builder
web/src             React + TanStack Query client
docker/             Container entrypoint (drops to PUID:PGID)
scripts/            Dev server launcher, icon generator, release version script

Paging the unified inbox: each source (account + folder) is consumed newest-UID-first. Pages are merged by date, and the cursor stores, per source, the UIDVALIDITY and the lowest UID returned so far. If a folder's UIDVALIDITY changes, the server tells the client to reload the list. Message actions carry the UIDVALIDITY too and are refused (HTTP 409) if it no longer matches. Cached folders use the same cursors, so a page can come from the cache and the next from the IMAP server.

Limitations

  • Conversations bring together the listed folder and Sent. A message of the conversation in another folder (moved there by a filter, say) isn't shown with it.
  • Live updates (IDLE) cover each account's INBOX. Other folders refresh when you open them, when the window regains focus, and every couple of minutes.
  • Sent copies are appended without the Bcc header.
  • Snoozing is a Ratatoskr feature: other mail apps see the messages in the Snoozed folder, but only Ratatoskr brings them back, and only while it is running (late ones come back when it starts). A woken message is a new copy in INBOX, so it gets a new UID.
  • No OAuth2 IMAP (XOAUTH2 for Gmail/Microsoft 365). Use app passwords where the provider supports them.
  • Out of scope: server-side filters, calendar/contacts, JMAP, PGP, local full-text search.

License

GNU Affero General Public License v3.0 (AGPL-3.0-only). If you run a modified version as a service for others, you must offer them its source code.

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/magnuspihl/ratatoskr:latest
Last Updated2026-10-23
First Seen2026-10-23

Runtime arguments

Web UI
http://[IP]:[PORT:8080]/
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUI PortPorttcp

Web interface port.

Target
8080
Default
8080
Value
8080
AppDataPathrw

Holds ratatoskr.db (users, accounts, settings) and cache.db (mail cache).

Target
/data
Default
/mnt/user/appdata/ratatoskr
Value
/mnt/user/appdata/ratatoskr
PUIDVariable

User ID the server runs as and that owns /data. The entrypoint chowns /data and drops privileges. Image default is 1000; Unraid uses 99.

Default
99
Value
99
PGIDVariable

Group ID the server runs as and that owns /data. Image default is 1000; Unraid uses 100.

Default
100
Value
100
UMASKVariable

Optional umask for files the server creates, e.g. 022.

Base URLVariable

Public https URL users reach Ratatoskr at, e.g. https://mail.example.com. Used for the OIDC redirect URI, the cookie Secure flag and the WebSocket origin check.

Target
RATATOSKR_BASE_URL
Secret KeyVariable

Master secret, at least 32 characters. Generate one with: openssl rand -base64 48. WARNING: changing it later makes stored mail account passwords unreadable.

Target
RATATOSKR_SECRET_KEY
Trust ProxyVariable

Trust X-Forwarded-* headers. Keep true when running behind a reverse proxy.

Target
RATATOSKR_TRUST_PROXY
Default
true
Value
true
Password LoginVariable

Optional. Enable username/password login (true/false). Leave empty for the default: true without OIDC, false with OIDC.

Target
RATATOSKR_PASSWORD_LOGIN
RegistrationVariable

Who may create a password account: open, first (first user of an empty instance only) or closed.

Target
RATATOSKR_REGISTRATION
Default
first
Value
first
OIDC Issuer URLVariable

Optional OIDC. Issuer URL; the discovery document is read from it. Register ${RATATOSKR_BASE_URL}/auth/callback as the redirect URI with your provider.

Target
OIDC_ISSUER_URL
OIDC Client IDVariable

Optional OIDC client ID.

Target
OIDC_CLIENT_ID
OIDC Client SecretVariable

Optional OIDC client secret (confidential clients).

Target
OIDC_CLIENT_SECRET
OIDC LabelVariable

Optional. Provider name shown on the login button. Empty uses the default (Single sign-on).

Target
OIDC_LABEL
OIDC ScopesVariable

Scopes requested from the OIDC provider.

Target
OIDC_SCOPES
Default
openid email profile
Value
openid email profile
Block Private Mail HostsVariable

Refuse IMAP/SMTP hosts resolving to private/internal addresses. Leave false when your mail server is on the LAN.

Target
RATATOSKR_BLOCK_PRIVATE_MAIL_HOSTS
Default
false
Value
false
Default IMAP HostVariable

Optional. Pre-fills the IMAP host in the Add account form. Accepts {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_IMAP_HOST
Default SMTP HostVariable

Optional. Pre-fills the SMTP host in the Add account form. Accepts {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_SMTP_HOST
Default IMAP SecurityVariable

Optional. tls, starttls or none (app default: tls).

Target
RATATOSKR_DEFAULT_IMAP_SECURITY
Default SMTP SecurityVariable

Optional. tls, starttls or none (app default: tls).

Target
RATATOSKR_DEFAULT_SMTP_SECURITY
Default IMAP PortVariable

Optional. Pre-fills the IMAP port. Accepts the {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_IMAP_PORT
Default SMTP PortVariable

Optional. Pre-fills the SMTP port. Accepts the {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_SMTP_PORT
Default Mail UsernameVariable

Optional. Pre-fills the mail username. Accepts {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_MAIL_USERNAME
Default EmailVariable

Optional. Pre-fills the email address, e.g. {username}@example.com. Accepts {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_EMAIL
Default Display NameVariable

Optional. Pre-fills the sender display name. Accepts {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_DISPLAY_NAME
Default Account LabelVariable

Optional. Pre-fills the account label. Accepts {username}, {email} and {name} tokens.

Target
RATATOSKR_DEFAULT_ACCOUNT_LABEL
CacheVariable

Keep a local copy of each account's newest INBOX and Sent mail. false always asks the mail server.

Target
RATATOSKR_CACHE
Default
true
Value
true
Cache MessagesVariable

Newest messages kept per cached folder (50-100000).

Target
RATATOSKR_CACHE_MESSAGES
Default
2000
Value
2000
Cache Max Body MBVariable

Largest message whose content is cached. 0 keeps no message content.

Target
RATATOSKR_CACHE_MAX_BODY_MB
Default
1
Value
1
Cache Account MBVariable

Message content kept per account; least recently read is removed first.

Target
RATATOSKR_CACHE_ACCOUNT_MB
Default
200
Value
200
Max Message MBVariable

Largest message the server downloads for display.

Target
RATATOSKR_MAX_MESSAGE_MB
Default
50
Value
50
Max Upload MBVariable

Largest total attachment upload when composing.

Target
RATATOSKR_MAX_UPLOAD_MB
Default
25
Value
25
IMAP Idle TimeoutVariable

Seconds before an unused pooled IMAP connection is closed.

Target
RATATOSKR_IMAP_IDLE_TIMEOUT
Default
300
Value
300
VAPID SubjectVariable

Optional. Contact sent to push services. Empty uses the base URL if https, else mailto:postmaster@host.

Target
RATATOSKR_VAPID_SUBJECT
Log LevelVariable

One of fatal, error, warn, info, debug, trace.

Target
LOG_LEVEL
Default
info
Value
info