What gets installed, how the server is wired, every config key, and honest security notes — so you know exactly what you are running.
DesertEmail is a one-binary, pure-Rust email server written from scratch: mail, IMAP, webmail, DKIM, DNS, and HTTP are hand-rolled pure std. The only dependency is rustls (plus rustls-pemfile and webpki-roots) for TLS — deliberately, because you should never roll your own crypto. A release build lands around ~1.2 MB stripped with rustls+ring.
It implements SMTP (inbound + authenticated submission), IMAP, webmail/admin over HTTP(S), DKIM signing, STARTTLS / implicit TLS, and an outbound MTA with MX lookup, opportunistic STARTTLS, and a disk retry queue — all with plain threads, not async.
Who it is for:
It is deliberately minimal. See Security notes before putting it on the public internet.
Default prefix is ~/.desertemail (override with DESERTEMAIL_PREFIX). On Windows: %USERPROFILE%\.desertemail.
$PREFIX/bin/desertemail (Windows: desertemail.exe)$PREFIX/config.toml (mode 0600 when the OS allows)$PREFIX/dkim.pem if you enable DKIM during the wizard$PREFIX/data by default (or DESERTEMAIL_DATA_DIR).profile). Markers are literal:# >>> desertemail PATH >>>
export PATH="$HOME/.desertemail/bin:$PATH"
# <<< desertemail PATH <<<
Fish uses set -gx PATH … between the same markers. Windows adds the bin directory to the user PATH via the registry (no shell-rc markers).
/etc/systemd/system/desertemail.service only if you opt in (Linux with systemd). The installer uses sudo only when installing that unit and you are not root.Safety guarantees of the installer:
/bin/ and verifies SHA-256 against /bin/SHA256SUMS when present (mismatch aborts; missing sums file warns and continues)sudo without a reason (systemd unit only) and, interactively, only after you ask for the unitconfig.toml without asking (non-interactive re-runs keep the existing config)systemctl stop desertemail if you installed the unit).rm -rf ~/.desertemail (or your DESERTEMAIL_PREFIX).sudo systemctl disable --now desertemail and sudo rm /etc/systemd/system/desertemail.service, then sudo systemctl daemon-reload.That is a complete removal. There are no other system packages, launch agents, or hidden services.
One process starts cooperating listeners (threads, shared config). Plaintext ports always bind; TLS extras bind only when tls_cert_file + tls_key_file load successfully and the listen address is non-empty:
0.0.0.0:2525 (real-world: 25); STARTTLS (RFC 3207) advertised when TLS is configured0.0.0.0:2587 (real: 587); STARTTLS when TLS is configured0.0.0.0:2143 (real: 143); STARTTLS (RFC 2595) when TLS is configured0.0.0.0:8080 (empty web_listen disables it)smtps_listen, e.g. 465), IMAPS (imaps_listen, e.g. 993), HTTPS webmail (web_tls_listen, e.g. 8443; Secure session cookies)data_dir/queue, MX/A lookup over UDP (or optional smarthost), opportunistic STARTTLS to remote MX with webpki-roots validation (falls back to plaintext on failure so delivery is never blocked), exponential backoff 1m → 5m → 15m → 1h → 4h, bounce after 24h, optional DKIM signing Internet / peers Clients (MUA / browser)
| |
v v
[ SMTP :25 ] [ submission :587 ]
(+ STARTTLS) [ IMAP :143 ] (+ STARTTLS)
| [ SMTPS :465 / IMAPS :993 ]
| [ web :8080 / HTTPS :8443 ]
v |
+-----------+ |
| Maildir | <------------------+
| storage |
+-----------+
|
v
[ queue worker ] --MX/smarthost--> remote MTAs
| (opportunistic STARTTLS)
DKIM sign (optional)
Storage is classic Maildir (cur/ new/ tmp/) per mailbox under data_dir. Easy to back up or rsync. STARTTLS upgrade resets protocol state and discards prior AUTH (RFC 3207 / RFC 2595).
Config is a hand-parsed TOML-like file (key = "value", simple lists, [users] section). Defaults below match src/config.rs Default.
| Key | Type | Default | Meaning |
|---|---|---|---|
domains | list of strings | ["localhost"] | Domains this server accepts mail for |
data_dir | string | "./data" | Maildir + queue root (absolute path recommended) |
smtp_listen | string | "0.0.0.0:2525" | Inbound SMTP bind address |
submission_listen | string | "0.0.0.0:2587" | Authenticated submission bind |
imap_listen | string | "0.0.0.0:2143" | IMAP bind address |
web_listen | string | "0.0.0.0:8080" | Webmail/admin HTTP bind; empty string disables |
admin_user | string / unset | unset | Login name allowed on /admin; empty/unset disables admin |
smarthost | optional string | none | Relay host:port when direct MX/port 25 is unavailable |
smarthost_user | optional string | none | AUTH user for smarthost |
smarthost_pass | optional string | none | AUTH password for smarthost |
catch_all | bool | true | Accept any local-part@domain and auto-create mailbox |
default_password | string | "changeme" | Password for catch-all / auto-created mailboxes |
dkim_selector | string | "mail" | DKIM selector; DNS name is <selector>._domainkey.<domain> |
dkim_key_file | optional string | none | Path to PEM RSA private key for signing |
tls_cert_file | optional string | none | PEM certificate chain; both this and tls_key_file required to enable TLS |
tls_key_file | optional string | none | PEM private key (PKCS#8 or RSA) |
smtps_listen | string | "" (disabled) | Implicit SMTPS bind (e.g. "0.0.0.0:465"); only bound when TLS cert/key loaded and non-empty |
imaps_listen | string | "" (disabled) | Implicit IMAPS bind (e.g. "0.0.0.0:993"); only bound when TLS loaded and non-empty |
web_tls_listen | string | "" (disabled) | HTTPS webmail bind (e.g. "0.0.0.0:8443"); Secure session cookies when used |
require_tls_for_auth | bool | false | If true, reject SMTP AUTH on plaintext with 538 |
[users] | map | empty | "local" or "user@domain" = plaintext password |
# Domains this server accepts (MX should point here)
domains = ["example.com", "mail.example.com"]
data_dir = "/home/you/.desertemail/data"
# High ports = no root. Real mail uses 25/587/143 (+ STARTTLS when TLS configured).
smtp_listen = "0.0.0.0:2525"
submission_listen = "0.0.0.0:2587"
imap_listen = "0.0.0.0:2143"
web_listen = "0.0.0.0:8080"
admin_user = "postmaster"
# Optional: when outbound port 25 is blocked
# smarthost = "smtp.example.com:587"
# smarthost_user = "you@example.com"
# smarthost_pass = "app-password"
catch_all = true
default_password = "changeme" # change this!
# openssl genrsa -out dkim.pem 2048
# desertemail --dkim-dns example.com
# dkim_selector = "mail"
# dkim_key_file = "dkim.pem"
# --- TLS (optional; both cert + key required to enable) ---
# openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
# -subj "/CN=mail.example.com" -keyout tls.key -out tls.crt
# tls_cert_file = "tls.crt"
# tls_key_file = "tls.key"
# smtps_listen = "0.0.0.0:465" # implicit SMTPS
# imaps_listen = "0.0.0.0:993" # implicit IMAPS
# web_tls_listen = "0.0.0.0:8443" # HTTPS webmail
# require_tls_for_auth = false # true => AUTH only over TLS (538 on plain)
[users]
"alice" = "alicepass"
"bob" = "bobpass"
"postmaster" = "adminpass"
CLI: desertemail --config path/to/config.toml (or -c). Help: --help / -h. DKIM DNS helper: desertemail --dkim-dns <domain> [--config path].
mail.example.com) → public IP (DynDNS is fine for home).example.com → mail.example.com (priority 10 is typical).v=spf1 mx a:mail.example.com ~all.dkim_key_file / dkim_selector, then:
desertemail --dkim-dns example.com --config ~/.desertemail/config.toml
Publish the printed TXT at <selector>._domainkey.example.com.
rDNS / PTR: many receivers check that your sending IP’s reverse DNS matches a sensible hostname. On a VPS, set PTR to your mail hostname when the provider allows it. Home ISPs rarely give you useful PTR.
Port 25: residential ISPs often block outbound 25. Receiving may still work with port-forward; for sending use a smarthost in config or host on a VPS with open egress on 25.
TLS is built in via rustls (ring crypto provider). There is no async runtime and no other crates for mail protocol code — only TLS uses a library. You supply a certificate and private key; DesertEmail does not talk to ACME / Let’s Encrypt for you.
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-subj "/CN=mail.example.com" -keyout tls.key -out tls.crt
Then set in config.toml:
tls_cert_file = "tls.crt"
tls_key_file = "tls.key"
Both paths must be set and load successfully. If either is missing or invalid, the server logs a warning and runs plaintext only (fine for LAN/localhost/behind VPN).
| Listener | Typical port | Mode |
|---|---|---|
smtp_listen | 25 (or 2525) | Plaintext; advertises STARTTLS (RFC 3207) when TLS configured |
submission_listen | 587 (or 2587) | Same — STARTTLS upgrade; AUTH required for submission |
imap_listen | 143 (or 2143) | Plaintext; advertises STARTTLS (RFC 2595) when TLS configured |
smtps_listen | 465 | Implicit TLS before banner (submission semantics); only if non-empty + TLS loaded |
imaps_listen | 993 | Implicit TLS before IMAP greeting |
web_listen | 8080 | HTTP webmail (no TLS on this socket) |
web_tls_listen | 8443 | HTTPS webmail; session cookie gets the Secure flag |
After STARTTLS, the server resets protocol state and discards any prior AUTH (you must re-authenticate under TLS).
Obtain a cert with certbot, acme.sh, or your host’s panel, then point DesertEmail at the PEM files:
# Example paths after certbot (adjust for your domain / layout)
tls_cert_file = "/etc/letsencrypt/live/mail.example.com/fullchain.pem"
tls_key_file = "/etc/letsencrypt/live/mail.example.com/privkey.pem"
smtps_listen = "0.0.0.0:465"
imaps_listen = "0.0.0.0:993"
web_tls_listen = "0.0.0.0:8443"
You own renewal (certbot timer, acme.sh cron, etc.). Restart or re-run DesertEmail after the files on disk change — there is no built-in reload/ACME client.
require_tls_for_auth = true
When true, SMTP AUTH on a plaintext connection is rejected with 538 Encryption required for requested authentication mechanism. Default is false so local demos still work without certs.
config.toml; TLS only protects the wirePlatform installers are generated from installers/template.sh (and a PowerShell / build-from-source sibling). There is no platform auto-detection and no GitHub Releases API in the install path — you pick a button; the script downloads /bin/desertemail-<rust-triple> from this site.
/bin/SHA256SUMS when available$PREFIX/bin and append the PATH block (idempotent markers)config.toml if missing / if you confirm overwriteopenssl for keygen)Use DESERTEMAIL_NONINTERACTIVE=1 with optional overrides:
| Variable | Purpose |
|---|---|
DESERTEMAIL_NONINTERACTIVE=1 | Skip prompts; use defaults / other env vars |
DESERTEMAIL_PREFIX | Install root (default ~/.desertemail) |
DESERTEMAIL_DOMAIN | Primary domain (default localhost) |
DESERTEMAIL_ADMIN_USER | Admin / first user (default admin) |
DESERTEMAIL_ADMIN_PASSWORD | Admin password (else random) |
DESERTEMAIL_DATA_DIR | Maildir root (default $PREFIX/data) |
DESERTEMAIL_WEBMAIL | 1/0 — enable web UI (default on) |
DESERTEMAIL_PORTS | high or privileged |
DESERTEMAIL_DKIM | 1/0 — generate/use DKIM key |
DESERTEMAIL_SYSTEMD | 1/0 — install unit (POSIX only; Windows has no systemd) |
curl -fsSL https://YOUR-SITE/install-linux-x86_64.sh \
| DESERTEMAIL_NONINTERACTIVE=1 DESERTEMAIL_DOMAIN=example.com sh
curl … | shirm https://YOUR-SITE/install-windows.ps1 | iex (or Bypass if execution policy blocks). Installs desertemail.exe, verifies with Get-FileHash, user PATH entrypkg install curl openssl (F-Droid Termux). High ports need no rootgit + Rust toolchain (cargo build --release); use when no prebuilt matches your machineDesertEmail is an educational / personal-use MVP — not a hardened production MTA. Be explicit about what it does and does not do.
tls_cert_file + tls_key_file to enable. Without them the server runs plaintext — fine for LAN/localhost/VPN; for any internet-facing deploy, configure TLS and consider require_tls_for_auth = true.config.toml. Keep the file chmod 600 and treat the prefix as secret. Hashing is planned, not present. TLS protects the wire, not the file on disk.default_password: with catch_all = true, any local-part authenticates with the default password. That is convenient for demos and dangerous if left as changeme on the internet.HttpOnly, SameSite=Lax; on HTTPS (web_tls_listen) the cookie also gets Secure. Tokens are derived from OS CSPRNG material (not just timestamp/PID).dkimpy in project tests.If you need a production mail gateway today, use Postfix/OpenSMTPD/etc. Use DesertEmail when you want ownership, tiny footprint, and a codebase you can actually finish reading.
tls_cert_file and tls_key_file (both required). You get STARTTLS on SMTP/IMAP, optional implicit ports (465/993), HTTPS webmail, and opportunistic outbound STARTTLS. There is no ACME auto-cert; bring your own files (self-signed, certbot, acme.sh, or a reverse proxy). See TLS / encryption setup.smarthost / smarthost_user / smarthost_pass, or host on a VPS with open egress. Inbound may still work with port-forward if you have a public IP.domains = ["a.com", "b.com"]. Users can be local-parts or full addresses.data_dir (default ~/.desertemail/data). Rsync-friendly; queue lives in data_dir/queue..exe + PowerShell installer. Maildir uses NTFS-safe naming (!2, instead of :2, in filenames)./bin/. The source is open source by request — text @bitfent on Telegram.std (no async runtime). The only crates are rustls (ring), rustls-pemfile, and webpki-roots for TLS — the one thing you should never hand-roll. Binary size is ~1.2 MB with TLS.“We can’t expect God to do all the work.”
— Joshua Graham