rmail

Production-grade mail server stack in pure Python 3 asyncio — SMTP, IMAP4rev1, and Exchange REST API.

Multi-Process Scaling

The shard-based storage model is designed for concurrent access. Multiple rmail processes can share the same mail_root directory on a POSIX filesystem without coordination beyond the short-duration fcntl.flock used for UID allocation.

For multi-process deployments, set exchange_api.token_secret explicitly to the same value in each instance's config.json. If left null, each process generates a different secret and tokens will not be valid across processes.

Known storage performance characteristics:

MetricCapacity / Latency
Messages per mailboxMillions (10M+ UIDs supported)
Flag change latency< 0.1 ms
Message delivery latency~2 ms
Concurrent flag changesUnlimited (atomic rename, CAS retry)
SELECT 100K messages~50 ms

TLS Configuration

Auto-Generated Self-Signed Certificates

By default, tls.cert_file is null. On startup, rmail calls ensure_tls(config), which invokes openssl req to generate a 4096-bit RSA key and a self-signed certificate valid for 10 years. Files are written to {mail_root}/tls/cert.pem and {mail_root}/tls/key.pem. Subsequent startups reuse the existing files.

Using a CA-Signed Certificate

Set the paths in config.json before starting the server:

{
  "tls": {
    "cert_file": "/etc/letsencrypt/live/mail.example.com/fullchain.pem",
    "key_file":  "/etc/letsencrypt/live/mail.example.com/privkey.pem"
  }
}

After setting these values, restart the server. TLS services (SMTPS 465, IMAPS 993, Exchange TLS 9003, STARTTLS on 25/587/143) activate automatically. AUTH capabilities are hidden from cleartext connections as a consequence.

Certificate Renewal

After renewing a certificate on disk, send SIGHUP to reload configuration without restarting listeners:

kill -HUP $(systemctl show -p MainPID rmail | cut -d= -f2)

Signal Handling

SignalEffect
SIGINTGraceful shutdown: stops all listeners and exits.
SIGTERMGraceful shutdown: stops all listeners and exits.
SIGHUPConfiguration reload: re-reads config.json and calls ensure_tls. Does not restart listeners.

Diagnostics

The diagnostic tool exercises every subsystem in sequence. Run it against a live server:

sudo python3 run.py diagnose

The 8 diagnostic phases are:

  1. Configuration & Environment — loads config, checks TLS cert validity and expiry, verifies all listening ports and the mailstore directory.
  2. User Management — creates a temporary diagnostic user, verifies directory layout, password hashing, default mailboxes, disabled-user rejection, admin flag toggle, and quota field.
  3. Backend Storage — appends, reads, flags, copies, and deletes messages; verifies shard placement and quota enforcement.
  4. SMTP Protocol — rate limit config, STARTTLS, AUTH, delivery on ports 25 and 587, external delivery, relay blocking, spoofing rejection, RFC compliance, EHLO AUTH hiding on cleartext.
  5. Delivery Verification — polls INBOX for messages sent in phase 4, verifying end-to-end delivery within 5 seconds; tests IDLE push notification.
  6. IMAP Protocol — CAPABILITY, LOGIN, LIST (wildcard and partial patterns), SPECIAL-USE, SELECT, FETCH, STORE, EXPUNGE, IDLE, ID, ENABLE, CHECK, UNSELECT, MOVE, RENAME with spaces, SEARCH complex criteria, COPY COPYUID, CAPABILITY AUTH hiding on cleartext.
  7. Exchange API — autoconfig, autodiscover, login, folder listing, message retrieval, flag update, send, append, pagination, OPTIONS CORS, 400 on bad request, non-admin alias 403, admin alias POST/DELETE, token expiry, rate limiter lockout.
  8. Cleanup — removes the diagnostic user and any aliases created during the run.

A healthy server produces 0 failures and 0 warnings.

Systemd Service

sudo python3 run.py install    # install and enable service
make start
make stop
make restart
make status
make logs                      # tail journald output

The install command writes a systemd unit file at /etc/systemd/system/rmail.service targeting the current working directory and Python interpreter, with Restart=on-failure and a 5-second restart delay. Review the unit file after installation to adjust WorkingDirectory, User, or resource limits for your deployment.

Delivery Queue Tuning

The delivery worker count controls how many messages can be written concurrently. Increase delivery.workers if throughput is bottlenecked by filesystem I/O on high-volume inbound workloads:

{
  "delivery": {
    "workers": 16
  }
}

Each worker is an asyncio coroutine; memory cost is minimal. Tune to match filesystem concurrency capacity.

Quota Management

Each user has a quota_mb field in user.json (default: 1024 MB). Quota is enforced on all append paths:

To update a user's quota, edit the quota_mb field in:

{mail_root}/domains/{domain}/users/{username}/user.json

Storage Backup

The mail_root directory is self-contained. A consistent backup can be taken with any tool that preserves filenames (rsync, tar, cp -a). Because flag changes use atomic rename within a single directory, a snapshot taken mid-operation will always contain complete, valid .eml files — at worst a flag state may be one operation behind.

Do not back up or restore individual shard directories while the server is processing flag changes on that mailbox, as the CAS retry loop may observe unexpected filenames. Stopping the server or using filesystem-level snapshots (LVM, ZFS) is recommended for consistent backups of active mailboxes.