diff --git a/README.md b/README.md
index 30c2768..6d9223b 100644
--- a/README.md
+++ b/README.md
@@ -5,138 +5,142 @@
RustMail
- A fast, feature-rich SMTP mail catcher built in Rust.
- Single binary. Persistent storage. Modern UI. CI-ready.
+ A fast, self-hosted SMTP mail catcher for development and testing.
+ Capture outbound email, inspect it in a modern UI, and assert on it in CI.
+
+
- ~7 MB binary · 28 KB JS gzipped · zero runtime dependencies
+ Documentation · API Reference · Docker Hub
-
-
- Web UI
- TUI
-
-
-
-
-
-
-
- Inspired by MailHog and MailCrab
+
-## Install
+## Quick Start
```sh
-brew install rustmailapp/rustmail/rustmail
+docker run -p 1025:1025 -p 8025:8025 smyile/rustmail:latest
```
-Or on Arch Linux (AUR):
+Point your app's SMTP at `localhost:1025`, then open [localhost:8025](http://localhost:8025). Emails appear in real time.
-```sh
-yay -S rustmail-bin
+### Docker Compose
+
+```yaml
+services:
+ rustmail:
+ image: smyile/rustmail:latest
+ ports:
+ - "1025:1025"
+ - "8025:8025"
+ volumes:
+ - rustmail-data:/data
+ restart: unless-stopped
+
+volumes:
+ rustmail-data:
```
-Or with Docker:
+## Install
+
+### Homebrew
```sh
-docker run -p 1025:1025 -p 8025:8025 smyile/rustmail:latest
+brew install rustmailapp/rustmail/rustmail
```
-Or from source:
+### Arch Linux (AUR)
```sh
-git clone https://github.com/rustmailapp/rustmail
-cd rustmail && make build
+yay -S rustmail-bin
```
-## Quick Start
+### From Source
```sh
-rustmail
+git clone https://github.com/rustmailapp/rustmail
+cd rustmail && make build
```
-Point your app's SMTP at `localhost:1025`, then open [localhost:8025](http://localhost:8025). Emails show up in real time.
+### Pre-built Binaries
-## Features
+Download from [GitHub Releases](https://github.com/rustmailapp/rustmail/releases/latest) — Linux (x86_64, aarch64, armv7 — glibc + musl), macOS (Intel + Apple Silicon), and multi-arch Docker images.
-### Core
+## Features
-- **Persistent storage** — SQLite-backed, emails survive restarts (or `--ephemeral` for CI)
-- **Full-text search** — FTS5 across subject, body, sender, and recipients
-- **Real-time updates** — WebSocket pushes new emails to the UI instantly
-- **Modern UI** — dark-mode-first, looks and feels like a real email client
+| | |
+|---|---|
+| **Persistent storage** | SQLite-backed, emails survive restarts. `--ephemeral` for CI. |
+| **Full-text search** | FTS5 across subject, body, sender, and recipients. |
+| **Real-time UI** | WebSocket push — new email appears instantly. Dark/light mode, keyboard shortcuts. |
+| **CI-native** | REST assertion endpoints, CLI assert mode, and a first-party GitHub Action. |
+| **Single binary** | Frontend embedded at compile time. ~7 MB, zero runtime dependencies. |
+| **Auth header display** | Parses DKIM, SPF, DMARC, and ARC headers with color-coded status badges. |
+| **Webhooks** | Fire-and-forget POST on every new message. |
+| **Email release** | Forward captured emails to a real SMTP server. |
+| **Export** | Download any email as EML or JSON. |
+| **Retention policies** | Auto-purge by age (`--retention`) or count (`--max-messages`). |
+| **TUI** | Optional terminal UI client for Neovim and headless workflows. |
-### CI/CD
+## GitHub Action
-- **REST assertion endpoints** — `GET /api/v1/assert/count?min=1&subject=Welcome`
-- **CLI assert mode** — `rustmail assert --min-count=2 --subject="Welcome" --timeout=30s`
-- **GitHub Action** — two-step start + assert, works on Linux and macOS runners
-- **Ephemeral mode** — in-memory DB for test pipelines, no cleanup needed
+```yaml
+- uses: rustmailapp/rustmail-action@v1
-### Advanced
+- run: npm test # your app sends emails to localhost:1025
-- **DKIM/SPF/DMARC/ARC display** — parses authentication headers, color-coded status badges (no other local catcher does this)
-- **Webhook notifications** — fire-and-forget POST on new email via `--webhook-url`
-- **Email release** — forward captured emails to a real SMTP server
-- **Export** — download as EML or JSON
+- uses: rustmailapp/rustmail-action@v1
+ with:
+ mode: assert
+ assert-count: 1
+ assert-subject: "Welcome"
+```
-### Platform Support
+## CLI Assert Mode
-Pre-built binaries and multi-arch Docker images for every major platform:
+Run as an ephemeral mail catcher that exits with a status code — for CI without the GitHub Action:
-| Platform | Architectures | Format |
-|----------|--------------|--------|
-| **Linux** | x86_64, aarch64, armv7 | gnu + musl (static) |
-| **macOS** | x86_64 (Intel), aarch64 (Apple Silicon) | native |
-| **Docker** | amd64, arm64, arm/v7 | multi-arch manifest |
+```sh
+rustmail assert --min-count=2 --subject="Welcome" --timeout=30s
+```
-Static musl builds run on Alpine, distroless, and any Linux — no glibc required.
+## Configuration
-### Developer Experience
+Configure via CLI flags, `RUSTMAIL_*` environment variables, or a TOML config file:
-- **Keyboard shortcuts** — `j`/`k` navigate, `d` delete, `D` clear all, `/` search, `Esc` close
-- **Dark/light theme** — toggle with localStorage persistence
-- **Flexible config** — CLI flags, `RUSTMAIL_*` env vars, or TOML config file
-- **Retention policies** — `--retention` (hours) and `--max-messages` with automatic enforcement
+```sh
+rustmail serve --bind 0.0.0.0 --smtp-port 2525 --http-port 9025
+rustmail serve --retention 24 --max-messages 1000
+rustmail serve --webhook-url https://hooks.example.com/email
+```
-## GitHub Action
+See the full [configuration reference](https://docs.rustmail.app/configuration/cli-flags).
-```yaml
-- name: Start RustMail
- uses: rustmailapp/rustmail-action@v1
+## TUI
-- name: Run tests (your app sends emails to localhost:1025)
- run: npm test
+
+
+
-- name: Assert emails were sent
- uses: rustmailapp/rustmail-action@v1
- with:
- mode: assert
- assert-count: 1
- assert-subject: "Welcome"
-```
+A terminal UI that connects to a running RustMail instance. Also available as a [Neovim plugin](https://docs.rustmail.app/integrations/neovim).
## Documentation
-Full docs at [docs.rustmail.app](https://docs.rustmail.app) ([source](docs/)):
+Full docs at [docs.rustmail.app](https://docs.rustmail.app):
-- [Getting Started](docs/getting-started/introduction.md)
-- [Configuration](docs/configuration/cli-flags.md)
-- [CI Integration](docs/ci-integration/rest-assertions.md)
-- [Features](docs/features/webhooks.md) — Webhooks, Export, Release, Email Auth, WebSocket
-- [Integrations](docs/integrations/neovim.md) — Neovim Plugin
-- [API Reference](docs/api/index.md)
-- [Docker](docs/getting-started/docker.md)
-- [Architecture](docs/getting-started/architecture.md)
+- [Getting Started](https://docs.rustmail.app/getting-started/introduction)
+- [Docker](https://docs.rustmail.app/getting-started/docker)
+- [Configuration](https://docs.rustmail.app/configuration/cli-flags)
+- [CI Integration](https://docs.rustmail.app/ci-integration/rest-assertions)
+- [API Reference](https://docs.rustmail.app/api/)
## License
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 0000000..e25959f
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,102 @@
+# RustMail
+
+A fast, feature-rich SMTP mail catcher built in Rust. Single binary. Persistent storage. Modern UI. CI-ready.
+
+**GitHub:** [rustmailapp/rustmail](https://github.com/rustmailapp/rustmail) · **Docs:** [docs.rustmail.app](https://docs.rustmail.app)
+
+## Quick Start
+
+```sh
+docker run -p 1025:1025 -p 8025:8025 smyile/rustmail:latest
+```
+
+Point your app's SMTP at `localhost:1025`, then open [localhost:8025](http://localhost:8025). Emails show up in real time.
+
+## Docker Compose
+
+```yaml
+services:
+ rustmail:
+ image: smyile/rustmail:latest
+ ports:
+ - "1025:1025"
+ - "8025:8025"
+ security_opt:
+ - no-new-privileges:true
+ volumes:
+ - rustmail-data:/data
+ restart: unless-stopped
+
+volumes:
+ rustmail-data:
+```
+
+## Supported Architectures
+
+| Architecture | Tag |
+|---|---|
+| `linux/amd64` | `latest` |
+| `linux/arm64` | `latest` |
+| `linux/arm/v7` | `latest` |
+
+Multi-arch manifest — Docker automatically pulls the correct image for your platform.
+
+## Persistence
+
+The image stores emails at `/data/rustmail.db` by default. Mount a volume to `/data` to persist emails across container restarts.
+
+For ephemeral (CI) usage, skip the volume:
+
+```sh
+docker run -p 1025:1025 -p 8025:8025 -e RUSTMAIL_EPHEMERAL=true smyile/rustmail:latest
+```
+
+## Environment Variables
+
+All configuration is done via `RUSTMAIL_*` environment variables:
+
+| Variable | Default | Description |
+|---|---|---|
+| `RUSTMAIL_BIND` | `0.0.0.0` | IP address to bind listeners to |
+| `RUSTMAIL_SMTP_PORT` | `1025` | SMTP listener port |
+| `RUSTMAIL_HTTP_PORT` | `8025` | HTTP and WebSocket port |
+| `RUSTMAIL_DB_PATH` | `/data/rustmail.db` | Path to SQLite database file |
+| `RUSTMAIL_RETENTION` | `0` | Auto-delete messages after N hours (`0` = keep forever) |
+| `RUSTMAIL_MAX_MESSAGES` | `0` | Max messages to retain (`0` = unlimited) |
+| `RUSTMAIL_MAX_MESSAGE_SIZE` | `10485760` | Max accepted message size in bytes (10 MB) |
+| `RUSTMAIL_EPHEMERAL` | `false` | Use in-memory SQLite (no data written to disk) |
+| `RUSTMAIL_WEBHOOK_URL` | — | URL to POST on every new message |
+| `RUSTMAIL_LOG_LEVEL` | `info` | Log verbosity: `trace`, `debug`, `info`, `warn`, `error` |
+| `RUSTMAIL_RELEASE_HOST` | — | Allowed SMTP target for email release (`host:port`) |
+
+## Ports
+
+| Port | Protocol | Description |
+|---|---|---|
+| `1025` | TCP | SMTP server |
+| `8025` | TCP | HTTP API, WebSocket, and Web UI |
+
+## Features
+
+- **Persistent storage** — SQLite-backed, emails survive restarts
+- **Full-text search** — FTS5 across subject, body, sender, and recipients
+- **Real-time updates** — WebSocket pushes new emails to the UI instantly
+- **Modern UI** — dark-mode-first, looks and feels like a real email client
+- **DKIM/SPF/DMARC/ARC display** — parses authentication headers with color-coded badges
+- **REST assertion endpoints** — `GET /api/v1/assert/count?min=1&subject=Welcome`
+- **Webhook notifications** — fire-and-forget POST on new email
+- **Email release** — forward captured emails to a real SMTP server
+- **Export** — download as EML or JSON
+- **Retention policies** — auto-purge by age or count
+
+## Image Details
+
+- **Base image:** `alpine:3.21`
+- **Runs as:** non-root user `rustmail`
+- **Healthcheck:** built-in (HTTP check every 30s)
+- **Volume:** `/data`
+- **Security:** `no-new-privileges` recommended
+
+## License
+
+Licensed under either of [MIT](https://github.com/rustmailapp/rustmail/blob/master/LICENSE-MIT) or [Apache 2.0](https://github.com/rustmailapp/rustmail/blob/master/LICENSE-APACHE), at your option.
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 05a6ac2..3901fe0 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -60,6 +60,7 @@ const reference: DefaultTheme.SidebarItem[] = [
{ text: "WebSocket", link: "/features/websocket" },
{ text: "Keyboard Shortcuts", link: "/features/keyboard-shortcuts" },
{ text: "Email Authentication", link: "/features/email-auth" },
+ { text: "Terminal UI", link: "/features/tui" },
],
},
{
@@ -151,7 +152,7 @@ export default defineConfig({
footer: {
message: "Released under the MIT / Apache 2.0 License.",
- copyright: "Copyright © 2026 Davide Tacchini",
+ copyright: "Copyright © 2026 Smyile",
},
},
});
diff --git a/docs/ci-integration/github-action.md b/docs/ci-integration/github-action.md
index f0525cf..40d19af 100644
--- a/docs/ci-integration/github-action.md
+++ b/docs/ci-integration/github-action.md
@@ -117,9 +117,11 @@ By default the action downloads the latest RustMail release. Pin to a specific v
```yaml
- uses: rustmailapp/rustmail-action@v1
with:
- version: v0.1.0
+ version: v0.2.1 # pin to a release tag
```
+Check [GitHub Releases](https://github.com/rustmailapp/rustmail/releases) for available versions.
+
::: tip
For non-GitHub CI systems, use the [CLI Assert Mode](/ci-integration/cli-assert) or [REST Assertions](/ci-integration/rest-assertions) directly.
:::
diff --git a/docs/features/tui.md b/docs/features/tui.md
new file mode 100644
index 0000000..241cb46
--- /dev/null
+++ b/docs/features/tui.md
@@ -0,0 +1,69 @@
+# Terminal UI
+
+RustMail includes a built-in terminal UI (TUI) that connects to a running RustMail instance. It provides a full email browser in your terminal with real-time updates via WebSocket.
+
+## Usage
+
+```sh
+rustmail tui
+```
+
+Connect to a specific host and port:
+
+```sh
+rustmail tui --host 192.168.1.10 --port 8025
+```
+
+::: info
+The `tui` subcommand requires the `tui` feature flag. Pre-built release binaries include it by default. Docker images do not (headless use case).
+:::
+
+## Options
+
+| Flag | Default | Description |
+|------|---------|-------------|
+| `--host` | `127.0.0.1` | RustMail server host to connect to |
+| `--port` | `8025` | RustMail HTTP port |
+
+## Keymaps
+
+### Message List
+
+| Key | Action |
+|-----|--------|
+| `j` / `Down` | Select next message |
+| `k` / `Up` | Select previous message |
+| `Enter` / `l` / `Right` | Open selected message |
+| `Tab` | Switch focus between list and preview |
+| `/` | Search |
+| `r` | Toggle read/unread |
+| `s` | Toggle starred |
+| `d` | Delete selected message |
+| `D` | Delete all messages (with confirmation) |
+| `R` | View raw message |
+| `g` | Jump to first message |
+| `G` | Jump to last message |
+| `]` | Next page |
+| `[` | Previous page |
+| `?` | Help |
+| `q` | Quit |
+
+### Message Preview
+
+| Key | Action |
+|-----|--------|
+| `j` / `Down` | Scroll down |
+| `k` / `Up` | Scroll up |
+| `Esc` / `h` / `Left` / `Tab` | Back to list |
+| `1` | Text tab |
+| `2` | Headers tab |
+| `3` | Raw tab |
+| `r` | Toggle read/unread |
+| `s` | Toggle starred |
+| `d` | Delete message |
+| `R` | View full raw source |
+| `q` | Quit |
+
+## Neovim Integration
+
+The TUI is also available as a Neovim plugin with floating windows and custom keymaps. See [rustmail.nvim](/integrations/neovim).
diff --git a/docs/getting-started/docker.md b/docs/getting-started/docker.md
index bcc45cc..5093401 100644
--- a/docs/getting-started/docker.md
+++ b/docs/getting-started/docker.md
@@ -17,8 +17,11 @@ services:
ports:
- "1025:1025"
- "8025:8025"
+ security_opt:
+ - no-new-privileges:true
volumes:
- rustmail-data:/data
+ restart: unless-stopped
volumes:
rustmail-data:
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 8e0c416..a546383 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -22,6 +22,10 @@ yay -S rustmail-bin
Or with any AUR helper (`paru -S rustmail-bin`, `pacman -S rustmail-bin` via chaotic-aur, etc.).
+## Pre-built Binaries
+
+Download from [GitHub Releases](https://github.com/rustmailapp/rustmail/releases/latest) — Linux (x86_64, aarch64, armv7 — glibc + musl), macOS (Intel + Apple Silicon).
+
## From Source
```sh
diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md
index c1f6e2e..751a74d 100644
--- a/docs/getting-started/introduction.md
+++ b/docs/getting-started/introduction.md
@@ -12,7 +12,7 @@ Most alternatives are either unmaintained (MailHog), minimal (MailCrab), or clou
- **Search that works** — FTS5 across subject, body, sender, and recipients.
- **Real-time UI** — WebSocket push, dark mode, keyboard shortcuts. Feels like a proper email client.
- **CI-first** — REST assertion endpoints, a CLI assert mode, and a GitHub Action.
-- **Zero runtime deps** — one binary, frontend baked in. Also ships as a ~15 MB Docker image.
+- **Zero runtime deps** — one binary, frontend baked in. Also ships as a ~8 MB Docker image.
## How It Works
diff --git a/docs/index.md b/docs/index.md
index 481736f..377a4a0 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -34,7 +34,7 @@ features:
- icon: ' '
title: One binary, no deps
details: The frontend is embedded at compile time. Drop it on a server and run it.
- - icon: ' '
- title: REST API + OpenAPI
- details: Full API with an OpenAPI 3.1 spec. Export emails, release to real SMTP, get webhook notifications.
+ - icon: ' '
+ title: DKIM / SPF / DMARC
+ details: Parses authentication headers with color-coded status badges. No other local mail catcher does this.
---
diff --git a/ui/src/components/MessageDetail.tsx b/ui/src/components/MessageDetail.tsx
index a11e204..be11401 100644
--- a/ui/src/components/MessageDetail.tsx
+++ b/ui/src/components/MessageDetail.tsx
@@ -113,7 +113,11 @@ export default function MessageDetail() {
false;
return (
api.markStarred(msg().id, !starred()).catch(() => {})}
+ onClick={() =>
+ api
+ .markStarred(msg().id, !starred())
+ .catch(() => {})
+ }
class="rounded-md border border-zinc-300 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-800 p-1.5 transition cursor-pointer"
classList={{
"text-amber-400 hover:text-amber-500": starred(),