Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ To go back, run `rustmail restore-backup` with the new binary **before** install
|---|---|
| **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. |
| **Real-time UI** | WebSocket push: new email appears instantly. Dark/light mode, five colour palettes, 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. |
| **Single binary** | Frontend embedded at compile time. 10 to 14 MB depending on the target, 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. |
Expand Down
2 changes: 2 additions & 0 deletions docs/ci-integration/cli-assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ rustmail assert [OPTIONS]
| `--sender` | — | Filter by sender address substring |
| `--recipient` | — | Filter by recipient address substring |
| `--smtp-port` | `1025` | SMTP port to listen on |
| `--max-message-size` | `10485760` | Maximum accepted message size in bytes |
| `--log-level` | `info` | Log verbosity: `trace`, `debug`, `info`, `warn`, `error` |

## Examples

Expand Down
4 changes: 2 additions & 2 deletions docs/ci-integration/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The action has two modes:
| `mode` | `start` | Set to `start` (or omit, it's the default) |
| `smtp-port` | `1025` | SMTP port to listen on |
| `http-port` | `8025` | HTTP/API port |
| `version` | `latest` | RustMail version (e.g., `v0.1.0`) |
| `version` | `latest` | RustMail version (e.g., `v0.8.1`) |

### Assert Mode

Expand Down Expand Up @@ -117,7 +117,7 @@ By default the action downloads the latest RustMail release. Pin to a specific v
```yaml
- uses: rustmailapp/rustmail-action@v1
with:
version: v0.2.1 # pin to a release tag
version: v0.8.1 # pin to a release tag
```

Check [GitHub Releases](https://github.com/rustmailapp/rustmail/releases) for available versions.
Expand Down
6 changes: 3 additions & 3 deletions docs/ci-integration/rest-assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ GET /api/v1/assert/count

| Parameter | Type | Description |
|-----------|------|-------------|
| `min` | integer | Minimum number of matching messages (inclusive) |
| `max` | integer | Maximum number of matching messages (inclusive) |
| `min` | integer | Minimum number of matching messages (inclusive). Defaults to `1`. |
| `max` | integer | Maximum number of matching messages (inclusive). No upper limit when omitted. |
| `subject` | string | Filter by subject substring (case-insensitive) |
| `sender` | string | Filter by sender address substring |
| `recipient` | string | Filter by recipient address substring |
Expand Down Expand Up @@ -41,7 +41,7 @@ curl -f "localhost:8025/api/v1/assert/count?min=1&recipient=admin@example.com"
{ "ok": true, "count": 2 }

// 417 Expectation Failed: assertion failed
{ "ok": false, "count": 0, "expected_min": 1, "expected_max": null }
{ "ok": false, "count": 0, "expected_min": 1, "expected_max": 9223372036854775807 }
```

## In a CI Script
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/toml-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ webhook_url = "https://hooks.example.com/email"
release_host = "smtp.example.com:587"
allowed_origins = ["https://mail.example.com"]
allowed_hosts = ["mail.example.com"]
ws_buffer = 4096
```

Configure both `smtp_tls_cert` and `smtp_tls_key` to enable optional SMTP `STARTTLS` on the existing SMTP listener. Setting only one of the two fails startup.
Expand Down
5 changes: 5 additions & 0 deletions docs/features/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ asks for confirmation instead, because clearing the inbox cannot be undone.
within it. Individual rows are not tab stops, so the list stays navigable no
matter how many rows are on screen.

While the list has keyboard focus, the selected row carries a rounded accent
outline; with no message selected, the outline goes around the whole list.
Clicking a row selects it without the outline, so the tinted background alone
marks the selection when you use the mouse.

| Key | Action |
|-----|--------|
| `↓` | Select next message |
Expand Down
2 changes: 1 addition & 1 deletion docs/features/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Release is locked down to prevent misuse:
2. **Host allowlist**: the `host` in the request body must exactly match the configured `--release-host` host. Mismatches return `403`.
3. **Port allowlist**: only standard SMTP ports are accepted: `25`, `465`, `587`, `2525`. Other ports return `400`.
4. **Port pinning**: if `--release-host` includes a port (e.g., `smtp.example.com:587`), the request must use that exact port. Mismatches return `403`.
5. **TLS required**: connections use `lettre::relay()` with certificate verification. TLS failures return `502`.
5. **TLS required**: port `465` uses implicit TLS; ports `25`, `587` and `2525` must upgrade with `STARTTLS`, and the message is not sent if the relay cannot. Certificates are verified. TLS failures return `502`.

## Errors

Expand Down
10 changes: 9 additions & 1 deletion docs/features/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ Below 1300px the details rail becomes a drawer over the message body. Open it wi
The gear icon in the header opens **Settings**. Under **Appearance**:

- **Theme**: **Dark**, **Light**, or **System** (the default, which follows the operating system).
- **Palette**: **RustMail** (the default), **Ember**, **Copper & Teal**, **Dawn**, or **Classic**. Each palette tints the neutrals and the accent colour, in both themes.
- **Palette**: sets the tint of the neutrals, the accent colour, the glow along the top of the window and the selected row, in both themes.

| Palette | Look |
|---------|------|
| **RustMail** (default) | Warm iron neutrals, an oxide orange accent, and an amber-to-oxide glow and selection |
| **Ember** | Cool slate neutrals with an orange accent |
| **Copper & Teal** | Teal-grey neutrals, an orange accent, and a copper and teal glow |
| **Dawn** | Rose-grey neutrals, a coral accent, and an amber-to-rose glow and selection |
| **Classic** | Plain zinc neutrals, an orange accent, and a grey selection |

Both choices are kept in the browser's local storage, so they apply per browser rather than per server.

Expand Down
12 changes: 7 additions & 5 deletions docs/features/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,43 +28,45 @@ Each webhook is an HTTP POST with `Content-Type: application/json`. The body is
{
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"sender": "user@example.com",
"recipients": "[\"recipient@example.com\"]",
"recipients": ["recipient@example.com"],
"subject": "Hello World",
"size": 1024,
"has_attachments": false,
"is_read": false,
"is_starred": false,
"tags": [],
"created_at": "2026-03-23T10:30:45.123Z"
"created_at": "2026-03-23T10:30:45Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | ULID: use this to fetch the full message via the REST API |
| `sender` | string | Envelope sender address |
| `recipients` | string | JSON-encoded array of recipient addresses |
| `recipients` | array of strings | Envelope recipient addresses |
| `subject` | string \| null | Parsed subject line |
| `size` | integer | Raw message size in bytes |
| `has_attachments` | boolean | Whether the message has attachments |
| `is_read` | boolean | Always `false` for new messages |
| `is_starred` | boolean | Always `false` for new messages |
| `tags` | array | Always `[]` for new messages |
| `created_at` | string | ISO 8601 UTC timestamp |
| `created_at` | string | ISO 8601 UTC timestamp, second precision |

## Behavior

- **Fire-and-forget**: the webhook is dispatched in a background task and does not block message processing.
- **5-second timeout**: if the endpoint doesn't respond within 5 seconds, the request is abandoned.
- **No retries**: failed deliveries are logged as warnings and not retried.
- **No queue**: webhooks are sent in real time as messages arrive. If the endpoint is down, those notifications are lost.
- **10 at a time**: at most 10 deliveries are in flight at once; further ones wait for a free slot.
- **No local or private IP literals**: RustMail refuses to start if the URL's host is `localhost` or a loopback, private, link-local or other reserved IP address. Host names are not resolved, so a name that points at such an address is accepted.

## Example: Slack Notification

Pair with a lightweight relay that transforms the payload into a Slack message:

```sh
rustmail serve --webhook-url http://localhost:3000/relay
rustmail serve --webhook-url https://relay.example.com/rustmail
```

The relay receives the JSON payload above and can POST a formatted message to Slack's Incoming Webhooks API.
16 changes: 12 additions & 4 deletions docs/features/websocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ RustMail pushes real-time events over a WebSocket connection. The UI uses this f
ws://localhost:8025/api/v1/ws
```

The connection is **one-way push**: the server sends events to the client. Messages sent by the client are ignored.
The connection is **one-way push**: the server sends events to the client. Messages sent by the client carry no meaning, but any frame from the client counts as a sign of life (see [Keepalive](#keepalive)).

## Events

Expand All @@ -28,12 +28,14 @@ Fired when a new email is received and stored.
"data": {
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"sender": "user@example.com",
"recipients": "[\"recipient@example.com\"]",
"recipients": ["recipient@example.com"],
"subject": "Hello World",
"size": 1024,
"has_attachments": false,
"is_read": false,
"created_at": "2026-03-23T10:30:45.123Z"
"is_starred": false,
"tags": [],
"created_at": "2026-03-23T10:30:45Z"
}
}
```
Expand Down Expand Up @@ -113,9 +115,15 @@ rustmail serve --allowed-origin https://mail.example.com

A maximum of **50 concurrent WebSocket connections** is enforced. New connections beyond this limit receive `503 Service Unavailable`.

## Keepalive

The server sends a ping when the connection opens and every 30 seconds after that. Browsers answer with a pong on their own. If no frame arrives from the client for 90 seconds, the server closes the connection. A send that blocks for more than 10 seconds closes it too, so a client that stopped reading cannot hold one of the 50 slots.

A client that falls more than [`--ws-buffer`](/configuration/cli-flags) events behind is disconnected instead of being sent the events that are left. Its view of the inbox is already incomplete at that point, so it should reconnect and fetch the list again.

## Reconnection

The built-in UI reconnects automatically with exponential backoff (2s initial, 30s cap), resetting on successful connection. If you're building a custom client, implement similar retry logic.
The built-in UI reconnects automatically with exponential backoff (2s initial, 30s cap) and random jitter, resetting on successful connection. If you're building a custom client, implement similar retry logic.

## Example: Node.js Client

Expand Down
8 changes: 4 additions & 4 deletions docs/getting-started/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ RustMail is a Cargo workspace with five crates, each with a single responsibilit

| Crate | Responsibility |
|-------|---------------|
| `rustmail-smtp` | TCP listener, ESMTP handshake, emits parsed messages over a tokio broadcast channel |
| `rustmail-smtp` | TCP listener, ESMTP handshake, hands each message to the server over a tokio mpsc channel and acknowledges it once stored |
| `rustmail-storage` | sqlx + SQLite repository, FTS5 index, retention enforcement |
| `rustmail-api` | Axum routes, WebSocket broadcast, bridges HTTP to storage and SMTP channel |
| `rustmail-api` | Axum routes and the WebSocket; bridges HTTP to storage and relays released mail |
| `rustmail-server` | Binary entry point: parses config, wires all crates, embeds UI assets |
| `rustmail-tui` | Terminal UI client (optional, connects to a running RustMail instance) |

Expand All @@ -19,7 +19,7 @@ SMTP Client (your app)
│
▼
rustmail-smtp
│ tokio broadcast channel (ReceivedMessage)
│ tokio mpsc channel (Delivery)
▼
rustmail-storage
│ event broadcast
Expand All @@ -44,4 +44,4 @@ rustmail-api

The UI is a SolidJS + TypeScript + Tailwind CSS v4 application in the `ui/` directory. At build time, Vite produces static assets that are embedded into the Rust binary via `rust-embed`. The server binary serves these at `/` with SPA fallback routing.

Build output: ~28 KB JS + ~13 KB CSS (gzipped).
Build output: ~48 KB JS + ~11 KB CSS (gzipped).
4 changes: 2 additions & 2 deletions docs/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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 ~8 MB Docker image.
- **Zero runtime deps**: one binary, frontend baked in. Also ships as a ~10 MB Docker image.

## How It Works

Expand All @@ -24,7 +24,7 @@ SMTP Client (your app)
▼
rustmail-smtp ─── tokio TCP listener, ESMTP handshake
│
▼ broadcast channel
▼ mpsc channel
rustmail-storage ─── sqlx + SQLite + FTS5
│
▼ event broadcast
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ docker run -p 1025:1025 -p 8025:8025 smyile/rustmail:latest
### Binary

```sh
# Default: SMTP on 1025, UI on 8025, SQLite at ./rustmail.db
# Default: SMTP on 1025, UI on 8025, SQLite in the user data directory
# (see --db-path in the configuration reference)
rustmail

# Ephemeral mode for CI: in-memory, nothing written to disk
Expand Down
Binary file modified docs/public/screenshot-web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading