elektromail 0.1.1

A minimal, Rust-based IMAP + SMTP mail server for local development and testing
Documentation

elektromail

crates.io License: EUPL-1.2

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:

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

docker run --rm -p 2525:2525 -p 1143:1143 \
  registry.gitlab.com/elektroladen/elektromail:latest

With custom credentials:

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:

cargo install elektromail

Or add as a dev-dependency for testing:

[dev-dependencies]
elektromail = "0.1"

From Source

git clone https://gitlab.com/elektroladen/elektromail.git
cd elektromail
cargo build --release

Quick Start

# 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

Variable Default Description
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:

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

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:

{"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:

[
  {
    "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:

HTTP_PORT=8080 cargo run
Endpoint Method Description
/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:

Tag Description
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

docker build -t elektromail:local .
docker run --rm -p 2525:2525 -p 1143:1143 elektromail:local

Docker Compose Example

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:

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.

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).

Test Coverage

210 tests covering all major IMAP commands:

Section Commands Tests
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

# Test the bundled elektromail server
cargo test rfc9051

Testing Against External IMAP Servers

The harness can validate any IMAP server:

# 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/:

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

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

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 by Marcel May. 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 file for details.