elektromail 0.1.1

A minimal, Rust-based IMAP + SMTP mail server for local development and testing
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# elektromail

[![crates.io](https://img.shields.io/crates/v/elektromail.svg)](https://crates.io/crates/elektromail)
[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](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

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

```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
```

| 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

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

| 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

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