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
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to RustMail

Thank you for your interest in contributing to RustMail! This document provides guidelines and instructions for contributing.
Thank you for your interest in contributing to RustMail!

## Getting Started

Expand Down Expand Up @@ -40,16 +40,16 @@ make dev

## Reporting Bugs

Before filing a bug report, please check [existing issues](https://github.com/rustmailapp/rustmail/issues). Then use the [Bug Report](https://github.com/rustmailapp/rustmail/issues/new?template=bug_report.yml) template it will guide you through the required information.
Before filing a bug report, please check [existing issues](https://github.com/rustmailapp/rustmail/issues). Then use the [Bug Report](https://github.com/rustmailapp/rustmail/issues/new?template=bug_report.yml) template: it will guide you through the required information.

## Submitting Changes

### Branch Naming

- `feat/short-description` New features
- `fix/short-description` Bug fixes
- `refactor/short-description` Code restructuring
- `docs/short-description` Documentation changes
- `feat/short-description`: New features
- `fix/short-description`: Bug fixes
- `refactor/short-description`: Code restructuring
- `docs/short-description`: Documentation changes

### Code Style

Expand All @@ -59,7 +59,7 @@ Before filing a bug report, please check [existing issues](https://github.com/ru
- No `unwrap()` in library crates (`crates/rustmail-smtp`, `crates/rustmail-storage`, `crates/rustmail-api`)
- `unwrap()` is acceptable in tests and `rustmail-server` (the binary)
- Error handling: `thiserror` in libraries, `anyhow` in the binary
- Async everywhere no blocking calls on the tokio runtime
- Async everywhere: no blocking calls on the tokio runtime

**TypeScript (UI):**
- Run `pnpm exec prettier --write .` before committing
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ cd rustmail && make build

### 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), and multi-arch Docker images.
Download from [GitHub Releases](https://github.com/rustmailapp/rustmail/releases/latest): Linux x86_64/aarch64/armv7 (glibc and musl builds), macOS (Intel + Apple Silicon), and multi-arch Docker images.

## Features

| Feature | Description |
|---|---|
| **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, 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. |
Expand All @@ -107,7 +107,7 @@ Download from [GitHub Releases](https://github.com/rustmailapp/rustmail/releases

## CLI Assert Mode

Run as an ephemeral mail catcher that exits with a status code for CI without the GitHub Action:
Run as an ephemeral mail catcher that exits with a status code, for CI without the GitHub Action:

```sh
rustmail assert --min-count=2 --subject="Welcome" --timeout=30s
Expand All @@ -124,7 +124,7 @@ rustmail serve --webhook-url https://hooks.example.com/email
rustmail serve --smtp-tls-cert ./certs/localhost.pem --smtp-tls-key ./certs/localhost-key.pem
```

STARTTLS is optional and uses the same SMTP port via explicit upgrade. RustMail advertises `STARTTLS` only when both the certificate and private key are configured.
STARTTLS uses the same SMTP port via explicit upgrade. Both `--smtp-tls-cert` and `--smtp-tls-key` are required; setting only one fails startup, and `STARTTLS` is advertised only when both are set.

See the full [configuration reference](https://docs.rustmail.app/configuration/cli-flags).

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ RustMail applies the following security practices:
- **Dependency pinning:** Workspace-level dependency management with locked versions
- **Input validation:** Bounded SMTP reads, FTS5 query sanitization, filename sanitization
- **Network security:** Default bind to `127.0.0.1`, configurable via `--bind`
- **CORS:** No CORS headers are sentbrowsers block all cross-origin access to the API; the UI is served same-origin
- **CORS:** No CORS headers are sent; browsers block all cross-origin access to the API, and the UI is served same-origin
- **Connection timeouts:** Per-operation idle timeout (60s) and overall session cap (5 min) on SMTP connections
- **Rate limiting:** Semaphore-based limits on SMTP sessions (100) and WebSocket connections (50)
- **Release safeguards:** Email release requires explicit `--release-host` flag with port allowlist
Expand All @@ -56,6 +56,6 @@ The following are in scope for security reports:

The following are out of scope:

- RustMail is a **development tool** it is not designed to be exposed to the public internet
- RustMail is a **development tool**: it is not designed to be exposed to the public internet
- Social engineering attacks
- Denial of service via legitimate high-volume email sending
8 changes: 4 additions & 4 deletions distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Reference: https://docs.brew.sh/Acceptable-Formulae

## Future Channels (not planned yet)

- **AUR** (Arch Linux) PKGBUILD, straightforward for Rust binaries
- **Nix** nixpkgs PR, similar community process to Homebrew
- **Scoop** (Windows) JSON manifest in a bucket repo
- **Snap / Flatpak** lower priority, Docker covers Linux well
- **AUR** (Arch Linux): PKGBUILD, straightforward for Rust binaries
- **Nix**: nixpkgs PR, similar community process to Homebrew
- **Scoop** (Windows): JSON manifest in a bucket repo
- **Snap / Flatpak**: lower priority, Docker covers Linux well
26 changes: 13 additions & 13 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ volumes:
| `linux/arm64` | `latest` |
| `linux/arm/v7` | `latest` |

Multi-arch manifest Docker automatically pulls the correct image for your platform.
Multi-arch manifest: Docker automatically pulls the correct image for your platform.

## Persistence

Expand Down Expand Up @@ -78,10 +78,10 @@ All configuration is done via `RUSTMAIL_*` environment variables:
| `1025` | TCP | SMTP server |
| `8025` | TCP | HTTP API, WebSocket, and Web UI |

`STARTTLS` uses the normal SMTP port and is advertised only when both `RUSTMAIL_SMTP_TLS_CERT` and `RUSTMAIL_SMTP_TLS_KEY` are set.

## Optional STARTTLS

STARTTLS uses the normal SMTP port via explicit upgrade. Both `RUSTMAIL_SMTP_TLS_CERT` and `RUSTMAIL_SMTP_TLS_KEY` are required; setting only one fails startup, and `STARTTLS` is advertised only when both are set.

Mount your TLS files read-only and set both environment variables:

```sh
Expand All @@ -97,16 +97,16 @@ Clients must issue `EHLO`, then `STARTTLS`, and then `EHLO` again after the TLS

## 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
- **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

Expand Down
4 changes: 2 additions & 2 deletions docs/ci-integration/cli-assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ rustmail assert --sender=notifications@example.com --min-count=1

| Code | Meaning |
|------|---------|
| `0` | Assertion passed required emails were received |
| `1` | Assertion failed timeout or criteria not met |
| `0` | Assertion passed: required emails were received |
| `1` | Assertion failed: timeout or criteria not met |

## How It Works

Expand Down
6 changes: 3 additions & 3 deletions docs/ci-integration/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ RustMail provides a GitHub Action for email assertion in CI pipelines.

The action has two modes:

1. **`start`** (default) Downloads the RustMail binary, starts an ephemeral SMTP server in the background, and waits until it's ready.
2. **`assert`** Checks captured emails against your filters using the REST assertion API.
1. **`start`** (default): Downloads the RustMail binary, starts an ephemeral SMTP server in the background, and waits until it's ready.
2. **`assert`**: Checks captured emails against your filters using the REST assertion API.

## Inputs

### Start Mode

| Input | Default | Description |
|-------|---------|-------------|
| `mode` | `start` | Set to `start` (or omit it's the default) |
| `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`) |
Expand Down
6 changes: 3 additions & 3 deletions docs/ci-integration/rest-assertions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# REST Assertions

RustMail exposes a purpose-built assertion endpoint for CI pipelines. It returns `200 OK` when conditions are met and `417 Expectation Failed` otherwise — designed for `curl -f`.
RustMail exposes a purpose-built assertion endpoint for CI pipelines. It returns `200 OK` when conditions are met and `417 Expectation Failed` otherwise. Designed for `curl -f`.

## Endpoint

Expand Down Expand Up @@ -37,10 +37,10 @@ curl -f "localhost:8025/api/v1/assert/count?min=1&recipient=admin@example.com"
## Response

```json
// 200 OK assertion passed
// 200 OK: assertion passed
{ "ok": true, "count": 2 }

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

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/cli-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Configuration is resolved in this precedence order: **CLI flags > environment va
| `--release-host` | `RUSTMAIL_RELEASE_HOST` | — | Allowed SMTP target for email release in `host:port` format (e.g. `smtp.example.com:587`). Release is disabled unless set. |
| `--config` | — | — | Path to an optional TOML configuration file. |

`STARTTLS` is advertised on the normal SMTP port only when both TLS paths are configured. After the client upgrades the connection, it must send `EHLO` again before continuing the session.
`STARTTLS` is advertised on the normal SMTP port only when both TLS paths are configured; setting only one fails startup. After the client upgrades the connection, it must send `EHLO` again before continuing the session.

## Examples

Expand Down
10 changes: 5 additions & 5 deletions docs/configuration/toml-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ webhook_url = "https://hooks.example.com/email"
release_host = "smtp.example.com:587"
```

Configure both `smtp_tls_cert` and `smtp_tls_key` to enable optional SMTP `STARTTLS` on the existing SMTP listener. If either value is missing, RustMail fails to start.
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.

## Precedence

Configuration is resolved in this order (highest wins):

1. **CLI flags** `--smtp-port 2525`
2. **Environment variables** `RUSTMAIL_SMTP_PORT=2525`
3. **TOML config file** `smtp_port = 2525`
4. **Defaults** `1025`
1. **CLI flags**: `--smtp-port 2525`
2. **Environment variables**: `RUSTMAIL_SMTP_PORT=2525`
3. **TOML config file**: `smtp_port = 2525`
4. **Defaults**: `1025`

This means you can set baseline config in a TOML file and override specific values with environment variables or CLI flags per-deployment.
2 changes: 1 addition & 1 deletion docs/features/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ GET /api/v1/messages/{id}/export?format=eml|json

### EML (default)

Returns the raw RFC 822 message the original bytes as received by the SMTP server.
Returns the raw RFC 822 message: the original bytes as received by the SMTP server.

```sh
curl -O -J "localhost:8025/api/v1/messages/01ARZ3NDEKTSV4RRFFQ69G5FAV/export"
Expand Down
6 changes: 3 additions & 3 deletions docs/features/html-preview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# HTML Preview

The message detail panel renders the email's HTML body so you see it the way a real client would — including remote images, which load automatically.
The message detail panel renders the email's HTML body so you see it the way a real client would.

## Rendering model

Expand All @@ -18,10 +18,10 @@ Because the iframe has no `allow-scripts` or `allow-same-origin`, email content

## Remote images

Remote images load automatically RustMail is a development mail catcher, so the "sender" is your own application and the priority is showing the message exactly as sent, with no extra click. Inline images referenced by `cid:` are served from the message's attachments.
Remote images load automatically. RustMail is a development mail catcher, so the "sender" is your own application and the priority is showing the message exactly as sent, with no extra click. Inline images referenced by `cid:` are served from the message's attachments.

Requests for remote content are sent with `Referrer-Policy: no-referrer`, so the originating URL is not leaked.

::: warning Exposed instances
Loading remote content means opening a message triggers an outbound request to whatever URLs it references. On the default loopback bind (`127.0.0.1`) this is harmless. If you bind RustMail to a non-loopback address (`RUSTMAIL_BIND`) and let it receive untrusted mail, be aware that viewing a message will fetch remote content from the viewer's network. The API is unauthenticated by design keep exposed instances behind your own access controls.
Loading remote content means opening a message triggers an outbound request to whatever URLs it references. On the default loopback bind (`127.0.0.1`) this is harmless. If you bind RustMail to a non-loopback address (`RUSTMAIL_BIND`) and let it receive untrusted mail, be aware that viewing a message will fetch remote content from the viewer's network. The API is unauthenticated by design: keep exposed instances behind your own access controls.
:::
12 changes: 6 additions & 6 deletions docs/features/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Or via environment variable:
RUSTMAIL_RELEASE_HOST=smtp.example.com:587 rustmail serve
```

The `--release-host` value is an allowlist only this exact host (and port, if specified) can be used as a release target. This prevents SSRF.
The `--release-host` value is an allowlist: only this exact host (and port, if specified) can be used as a release target. This prevents SSRF.

## Endpoint

Expand Down Expand Up @@ -45,11 +45,11 @@ Success response:

Release is locked down to prevent misuse:

1. **Disabled unless configured** returns `403` if `--release-host` is not set.
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`.
1. **Disabled unless configured**: returns `403` if `--release-host` is not set.
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`.

## Errors

Expand Down
10 changes: 5 additions & 5 deletions docs/features/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Each webhook is an HTTP POST with `Content-Type: application/json`. The body is

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | ULID use this to fetch the full message via the REST API |
| `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 |
| `subject` | string \| null | Parsed subject line |
Expand All @@ -54,10 +54,10 @@ Each webhook is an HTTP POST with `Content-Type: application/json`. The body is

## 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.
- **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.

## Example: Slack Notification

Expand Down
4 changes: 2 additions & 2 deletions docs/features/websocket.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# WebSocket

RustMail pushes real-time events over a WebSocket connection. The UI uses this for live inbox updates — you can use the same endpoint for custom integrations.
RustMail pushes real-time events over a WebSocket connection. The UI uses this for live inbox updates. You can use the same endpoint for custom integrations.

## Endpoint

```
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 are ignored.

## Events

Expand Down
6 changes: 3 additions & 3 deletions docs/getting-started/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RustMail is a Cargo workspace with five crates, each with a single responsibilit
| `rustmail-smtp` | TCP listener, ESMTP handshake, emits parsed messages over a tokio broadcast channel |
| `rustmail-storage` | sqlx + SQLite repository, FTS5 index, retention enforcement |
| `rustmail-api` | Axum routes, WebSocket broadcast, bridges HTTP to storage and SMTP channel |
| `rustmail-server` | Binary entry point parses config, wires all crates, embeds UI assets |
| `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) |

## Data Flow
Expand All @@ -33,11 +33,11 @@ rustmail-api

| Decision | Rationale |
|----------|-----------|
| Custom SMTP over samotop | A mail catcher needs minimal ESMTP samotop is over-engineered for this use case |
| Custom SMTP over samotop | A mail catcher needs minimal ESMTP; samotop is over-engineered for this use case |
| sqlx over rusqlite | Async-native, compile-time checked queries |
| ULID over UUID/integer | Time-sortable without needing a `created_at` index |
| SolidJS over Leptos/Yew | Contributor-friendly (JS/TS), smaller bundles, better ecosystem |
| rust-embed for UI | Single binary distribution no separate static file server needed |
| rust-embed for UI | Single binary distribution: no separate static file server needed |
| Ephemeral mode via `--ephemeral` | Same sqlx code path, just `sqlite::memory:` connection string |

## Frontend
Expand Down
Loading