# elektromail
[](https://crates.io/crates/elektromail)
[](LICENSE)
A minimal IMAP/SMTP mail server written in Rust, designed for development and testing purposes.
## Features
- IMAP4rev2 server on port 1143 (default)
- SMTP server on port 2525 (default)
- Auto-creates mailboxes when emails arrive
- In-memory storage (default) or SQLite persistence
- Plain text authentication (no TLS required for local dev)
- Optional STARTTLS with auto-generated certificates
- HTTP control plane for test automation
- Multi-architecture Docker images (amd64/arm64)
- Zero configuration required
- RFC 9051 compliance test harness
## Installation
### Docker (Recommended)
Pull from GitLab Container Registry:
```bash
# Pull latest image (multi-arch: amd64/arm64)
docker pull registry.gitlab.com/elektroladen/elektromail:latest
# Or pull a specific version
docker pull registry.gitlab.com/elektroladen/elektromail:v0.1.0
```
Run the container:
```bash
docker run --rm -p 2525:2525 -p 1143:1143 \
registry.gitlab.com/elektroladen/elektromail:latest
```
With custom credentials:
```bash
docker run --rm -p 2525:2525 -p 1143:1143 \
-e ELEKTROMAIL_USERS=myuser:mypass \
registry.gitlab.com/elektroladen/elektromail:latest
```
### Cargo (crates.io)
Install as a CLI tool:
```bash
cargo install elektromail
```
Or add as a dev-dependency for testing:
```toml
[dev-dependencies]
elektromail = "0.1"
```
### From Source
```bash
git clone https://gitlab.com/elektroladen/elektromail.git
cd elektromail
cargo build --release
```
## Quick Start
```bash
# Run the server
cargo run
# Run with custom credentials
ELEKTROMAIL_USERS=demo:demo cargo run
# Run all tests
cargo test
# Run RFC 9051 compliance tests only
cargo test rfc9051
```
## Configuration
### Environment Variables
| `ELEKTROMAIL_USERS` | `user:pass` | Comma-separated user credentials |
| `ELEKTROMAIL_AUTH_DISABLED` | - | Set to `1`, `true`, or `yes` to accept any credentials |
| `ELEKTROMAIL_DB` | - | SQLite database path for persistent storage |
| `ELEKTROMAIL_HTTP_TOKEN` | - | Bearer token required for HTTP control plane access |
| `ELEKTROMAIL_PRELOAD_DIR` | - | Filesystem directory for preloaded fixtures |
| `ELEKTROMAIL_DSN_RULES` | - | JSON rules for delivery policy/DSN generation |
| `SMTP_PORT` | `2525` | SMTP listen port |
| `IMAP_PORT` | `1143` | IMAP listen port |
| `HTTP_PORT` | - | HTTP control plane port (disabled if not set) |
| `BIND_ADDR` | `0.0.0.0` | Bind address |
### User Provisioning
Configure users via `ELEKTROMAIL_USERS` in comma-separated format:
```bash
# Single user
ELEKTROMAIL_USERS=admin:adminpass
# Multiple users
ELEKTROMAIL_USERS=admin:admin,demo:demo,test:test
# Extended format with email address
ELEKTROMAIL_USERS=admin:admin@example.com:adminpass,demo:demo@example.com:demopass
```
If not configured, defaults to `user` / `pass`.
Runtime user provisioning is available through the HTTP control plane (`POST /users`). Users added
there are available immediately for SMTP/IMAP authentication, and `POST /reset` restores the seed
users from `ELEKTROMAIL_USERS`.
### Storage
**In-memory (default):** All data is lost when the server stops. Suitable for most test scenarios.
**SQLite (persistent):** Set `ELEKTROMAIL_DB` to enable:
```bash
ELEKTROMAIL_DB=/path/to/mail.db cargo run
```
### Preloading Fixtures
Seed messages from disk by setting `ELEKTROMAIL_PRELOAD_DIR` to a directory structure like:
```
fixtures/
user/
INBOX/
welcome.eml
welcome.eml.meta.json
```
Each `.eml` file is appended to the specified mailbox. An optional `.eml.meta.json` sidecar can
include IMAP flags and an internal date:
```json
{"flags":["\\Seen","\\Flagged"],"internal_date":"01-Jan-2024 00:00:00 +0000"}
```
The preloaded messages become the baseline for `POST /reset`.
### Delivery Policy / DSN Rules
You can simulate bounces and rejections by configuring `ELEKTROMAIL_DSN_RULES` with JSON rules.
Rules support simple `*` wildcards on `recipient` and `sender` patterns.
Example:
```json
[
{
"recipient": "reject@example.com",
"action": "reject",
"code": 550,
"status": "5.1.1",
"diagnostic": "User unknown"
},
{
"recipient": "bounce@example.com",
"action": "bounce",
"status": "5.1.1",
"diagnostic": "Mailbox unavailable"
}
]
```
Supported actions:
- `reject` returns an SMTP 5xx response during `RCPT TO`.
- `bounce` accepts `RCPT TO`/`DATA`, drops delivery, and sends a DSN to the sender.
- `accept` explicitly allows delivery.
DSN messages are delivered to the sender’s mailbox (local part of `MAIL FROM`).
### Authentication
Both IMAP and SMTP support plain text authentication without TLS:
- **IMAP:** `LOGIN` command and `AUTHENTICATE PLAIN`
- **SMTP:** `AUTH PLAIN` mechanism
STARTTLS is available but optional. Connections start unencrypted and authentication works before any TLS upgrade. Certificates are auto-generated.
For tests that don't need auth validation, set `ELEKTROMAIL_AUTH_DISABLED=true` to accept any credentials.
### HTTP Control Plane
Enable by setting `HTTP_PORT`:
```bash
HTTP_PORT=8080 cargo run
```
| `/reset` | POST | Clear all mailboxes |
| `/purge` | POST | Clear all messages, keep users/mailboxes |
| `/inject` | POST | Inject email directly (JSON body) |
| `/users` | GET | List configured users |
| `/users` | POST | Create a user |
| `/config` | GET | Server configuration snapshot |
| `/messages` | GET | List all messages |
| `/messages?user=X&mailbox=Y` | GET | List messages for specific mailbox |
| `/messages/{uid}` | GET | Fetch a single message by UID |
| `/openapi.json` | GET | OpenAPI specification |
| `/docs` | GET | Swagger UI for the HTTP API |
`/users` supports `include_email=true` to return user entries with optional email fields.
Optional authentication for the HTTP API can be enabled with:
```
ELEKTROMAIL_HTTP_TOKEN=your-token
```
When set, requests must include `Authorization: Bearer your-token`. For browser access,
`/docs?token=your-token` and `/openapi.json?token=your-token` are also accepted, and the Swagger UI
will inject the header automatically.
## Docker
### Pre-built Images
Multi-architecture images (amd64/arm64) are available from GitLab Container Registry:
| `registry.gitlab.com/elektroladen/elektromail:latest` | Latest release |
| `registry.gitlab.com/elektroladen/elektromail:v0.1.0` | Specific version |
Images are based on Alpine Linux 3.21 for minimal footprint. The binary is statically compiled and stripped. Runs as non-root user.
### Build Locally
```bash
docker build -t elektromail:local .
docker run --rm -p 2525:2525 -p 1143:1143 elektromail:local
```
### Docker Compose Example
```yaml
services:
mail:
image: registry.gitlab.com/elektroladen/elektromail:latest
ports:
- "2525:2525" # SMTP
- "1143:1143" # IMAP
environment:
- ELEKTROMAIL_USERS=testuser:testpass
```
With custom ports and HTTP control plane:
```yaml
services:
mail:
image: registry.gitlab.com/elektroladen/elektromail:latest
ports:
- "3025:3025" # SMTP on custom port
- "3143:3143" # IMAP on custom port
- "8080:8080" # HTTP control plane
environment:
- SMTP_PORT=3025
- IMAP_PORT=3143
- HTTP_PORT=8080
- ELEKTROMAIL_USERS=admin:admin,demo:demo,test:test
```
### CI/CD Integration
For Docker-in-Docker setups, set `ELEKTROMAIL_DOCKER_HOST=docker`.
```bash
ELEKTROMAIL_DOCKER_TESTS=1 cargo test docker_procedere_smoke -- --nocapture
```
## RFC 9051 Compliance Test Harness
This project includes a comprehensive test harness for validating IMAP server implementations against [RFC 9051 (IMAP4rev2)](https://www.rfc-editor.org/rfc/rfc9051.html).
### Test Coverage
**210 tests** covering all major IMAP commands:
| 6.1 Any State | CAPABILITY, NOOP, LOGOUT | 19 |
| 6.2 Not Authenticated | LOGIN, STARTTLS, AUTHENTICATE | 22 |
| 6.3 Authenticated | SELECT, EXAMINE, CREATE, DELETE, RENAME, SUBSCRIBE, LIST, NAMESPACE, STATUS, APPEND, IDLE, ENABLE | 92 |
| 6.4 Selected | CLOSE, UNSELECT, EXPUNGE, SEARCH, FETCH, STORE, COPY, MOVE | 77 |
### Testing Against Local Server
```bash
# Test the bundled elektromail server
cargo test rfc9051
```
### Testing Against External IMAP Servers
The harness can validate any IMAP server:
```bash
# Test against an external server
RFC9051_TEST_HOST=imap.example.com \
RFC9051_TEST_PORT=993 \
RFC9051_TEST_USER=testuser \
RFC9051_TEST_PASS=testpass \
RFC9051_TEST_TLS=true \
cargo test rfc9051
```
### Test Organization
Tests are organized by RFC section:
```
tests/rfc9051/
├── mod.rs # Configuration and test client
├── client.rs # IMAP test client with TLS support
├── response.rs # Response parser
├── assertions.rs # RFC-specific assertions
├── fixtures.rs # Test messages
└── section6_commands/
├── any_state/ # CAPABILITY, NOOP, LOGOUT
├── not_authenticated/ # LOGIN, STARTTLS, AUTHENTICATE
├── authenticated/ # SELECT, EXAMINE, CREATE, etc.
└── selected/ # FETCH, STORE, SEARCH, etc.
```
### RFC Documentation
Local reference documentation is available in [docs/rfc9051/](docs/rfc9051/):
- [Section 1: Introduction](docs/rfc9051/section1_introduction.md)
- [Section 2: Protocol Overview](docs/rfc9051/section2_protocol.md)
- [Section 3: State Machine](docs/rfc9051/section3_state.md)
- [Section 4: Data Formats](docs/rfc9051/section4_data_formats.md)
- [Section 5: Operational](docs/rfc9051/section5_operational.md)
- [Section 6: Commands](docs/rfc9051/section6_commands.md)
- [Section 7: Responses](docs/rfc9051/section7_responses.md)
## IMAP Commands Reference
### Any State
- `CAPABILITY` - List server capabilities
- `NOOP` - No operation (keepalive)
- `LOGOUT` - End session
### Not Authenticated
- `LOGIN user pass` - Authenticate with credentials
- `STARTTLS` - Upgrade to TLS
- `AUTHENTICATE mechanism` - SASL authentication
### Authenticated
- `SELECT mailbox` - Open mailbox read-write
- `EXAMINE mailbox` - Open mailbox read-only
- `CREATE mailbox` - Create new mailbox
- `DELETE mailbox` - Delete mailbox
- `RENAME old new` - Rename mailbox
- `SUBSCRIBE mailbox` - Subscribe to mailbox
- `UNSUBSCRIBE mailbox` - Unsubscribe from mailbox
- `LIST ref pattern` - List mailboxes
- `NAMESPACE` - Get namespace prefixes
- `STATUS mailbox (items)` - Get mailbox status
- `APPEND mailbox message` - Add message to mailbox
- `IDLE` - Wait for updates
- `ENABLE capability` - Enable extension
### Selected
- `CLOSE` - Close mailbox, expunge deleted
- `UNSELECT` - Close mailbox, keep deleted
- `EXPUNGE` - Remove deleted messages
- `SEARCH criteria` - Find messages
- `FETCH seq items` - Retrieve message data
- `STORE seq flags` - Modify message flags
- `COPY seq mailbox` - Copy messages
- `MOVE seq mailbox` - Move messages
## Development
### Running Specific Tests
```bash
# Run a specific command's tests
cargo test rfc9051::section6_commands::authenticated::select
# Run with output
cargo test rfc9051 -- --nocapture
# Run single-threaded (useful for debugging)
cargo test rfc9051 -- --test-threads=1
```
### Adding New Tests
1. Create a new test file in the appropriate `section6_commands/` subdirectory
2. Add the module to the parent `mod.rs`
3. Use the `TestClient` from `crate::rfc9051`
4. Use assertions from `crate::rfc9051::assertions`
Example:
```rust
use crate::rfc9051::{TestClient, TestConfig};
use crate::rfc9051::assertions::*;
#[tokio::test]
async fn test_my_command() -> std::io::Result<()> {
let server = Server::start(Default::default()).await?;
let mut client = TestClient::connect(TestConfig::for_addr(server.imap_addr())).await?;
client.login_default().await?;
let response = client.send_command("MY COMMAND").await?;
assert_ok(&response, "MY COMMAND");
client.logout().await?;
server.stop().await?;
Ok(())
}
```
## Credits
This project was inspired by [GreenMail](https://github.com/greenmail-mail-test/greenmail/) by [Marcel May](https://github.com/marcelmay). GreenMail is an excellent Java-based email server for testing purposes. elektromail aims to provide a similar experience for developers who prefer a Rust-based solution.
## License
This project is licensed under the European Union Public Licence v1.2 (EUPL-1.2) - see the [LICENSE](LICENSE) file for details.