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.~/Library/LaunchAgents/org.desertemail.plist when you accept “Start DesertEmail now” (or DESERTEMAIL_AUTOSTART=1). Survives reboots via RunAtLoad.$PREFIX/desertemail.log when the installer (or launchd) starts the serverSafety 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)Easiest — one command (shows a summary, asks before deleting; mail data is kept unless you confirm):
curl -fsSL https://desertemail.org/uninstall.sh | sh
Non-interactive (CI / scripts): set DESERTEMAIL_NONINTERACTIVE=1 and DESERTEMAIL_UNINSTALL=1; add DESERTEMAIL_PURGE_DATA=1 only if you also want mail data removed. Honor DESERTEMAIL_PREFIX if you installed elsewhere.
systemctl stop desertemail (Linux unit), launchctl bootout gui/$(id -u)/org.desertemail (macOS agent), or kill the background process.rm -rf ~/.desertemail (or your DESERTEMAIL_PREFIX). Keep data/ if you want to preserve mail.sudo systemctl disable --now desertemail and sudo rm /etc/systemd/system/desertemail.service, then sudo systemctl daemon-reload.~/Library/LaunchAgents/org.desertemail.plist if present.That is a complete removal. There are no other system packages or hidden services beyond the optional unit/agent above.
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 |
spf_enforce | bool | false | When true, SPF hard-Fail + DMARC reject policy may 550; otherwise annotate only |
dmarc_enforce | bool | false | When true, honor DMARC p=reject (550) / p=quarantine (tag); default annotate only |
greylist | bool | false | Inbound greylisting: first triplet sight → 451 |
greylist_delay_secs | u64 | 60 | Minimum wait before greylist retry is accepted |
greylist_ttl_secs | u64 | 2592000 | Whitelist TTL after successful retry (30 days) |
dnsbls | list | [] | DNSBL zones (e.g. zen.spamhaus.org) |
dnsbl_reject | bool | false | When true, a DNSBL hit alone causes 550 |
spam_score_tag | i32 | 5 | Score ≥ this adds X-Spam-Flag: YES |
spam_score_reject | i32 | 0 | Score ≥ this → 550; 0 disables reject |
spam_folder_threshold | i32 | 4 | Score ≥ this delivers to Spam (.Junk); 0 disables |
spam_check_ptr | bool | true | Include missing/mismatched rDNS in spam score |
default_quota_mb | u64 | 0 | Default mailbox quota in MiB; 0 = unlimited. Over-quota: SMTP 452 4.2.2, IMAP APPEND NO [OVERQUOTA] |
log_format | string | "text" | "text" or "json" (one object per line: ts, level, msg, fields). Auth failures: event=auth_fail |
acme | bool | false | Enable ACME v2 auto TLS (Let's Encrypt). Non-blocking background thread |
acme_email | string | "" | Account contact (required when acme=true) |
acme_directory | string | LE production | ACME directory URL; use staging for tests |
acme_domains | list | domains | Hostnames on the certificate |
max_message_bytes | u64 | 26214400 (25 MiB) | Max SMTP DATA / IMAP APPEND size; oversize → SMTP 552 / IMAP NO |
metrics_token | string | "" | If non-empty, GET /metrics requires Authorization: Bearer … or ?token= |
[users] | map | empty | "local" or "user@domain" = password or PBKDF2 hash |
[quotas] | map | empty | Per-user quota overrides in MiB (e.g. "alice" = 512) |
IMAP4rev1 subset: LOGIN, SELECT/EXAMINE, LIST, FETCH, SEARCH (ALL/SEEN/UNSEEN/NEW/OLD/RECENT/FROM/TO/SUBJECT/BODY/TEXT/SINCE/BEFORE/HEADER + UID SEARCH), STORE (±FLAGS), EXPUNGE, CLOSE, APPEND (sync + non-sync literals), IDLE (RFC 2177, polls Maildir ~2s), UID FETCH/STORE/SEARCH/COPY, CAPABILITY (advertises IDLE), STARTTLS. UIDs are stable (filename-hash, flags stripped); UIDVALIDITY is constant.
Set acme = true, acme_email, tls_cert_file, tls_key_file, and ensure web_listen is reachable for HTTP-01 at /.well-known/acme-challenge/<token> (typically port 80). Account key is stored under {data_dir}/acme/account.key. Issuance runs in a background thread after listeners start and re-checks every 12h (renew when <30 days remain). Failures are logged; the server continues with any existing cert or plaintext.
Staging for first tests: acme_directory = "https://acme-staging-v02.api.letsencrypt.org/directory". A full live run needs a public domain and open port 80 — not available in most CI/sandbox environments.
With log_format = "json" (or text with level tags), auth failures emit event=auth_fail plus ip/user/proto. Example filter and jail: deploy/fail2ban-desertemail.conf and deploy/fail2ban-jail-desertemail.local.
# 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)
default_quota_mb = 0
log_format = "text"
# acme = true
# acme_email = "admin@example.com"
# acme_directory = "https://acme-staging-v02.api.letsencrypt.org/directory"
[users]
"alice" = "alicepass"
"bob" = "bobpass"
"postmaster" = "adminpass"
# [quotas]
# "alice" = 512
CLI: desertemail --config path/to/config.toml (or -c). Help: --help / -h. DKIM DNS helper: desertemail --dkim-dns <domain> [--config path]. User management: desertemail user add|remove|list|passwd. Domain/HTTPS from SSH: desertemail setup domain|dkim|https (see Domain & HTTPS setup and Operations).
Day-2 ops: add users without hand-editing config, back up Maildirs, scrape health/metrics, fail2ban, and restart under systemd.
Prefer the CLI or admin forms over editing [users] by hand. Both paths rewrite only the [users] / [quotas] blocks (atomic temp file + rename) and store PBKDF2 hashes.
desertemail --config /etc/desertemail/config.toml user add alice@example.com
desertemail --config /etc/desertemail/config.toml user add bob --password 'longer-secret' --quota 512
desertemail --config /etc/desertemail/config.toml user list
desertemail --config /etc/desertemail/config.toml user passwd alice
desertemail --config /etc/desertemail/config.toml user rename bob robert
desertemail --config /etc/desertemail/config.toml user remove bob
On the webmail Admin page (admin_user only): add user (email + password), reset a user's password (optionally logging out their webmail sessions), log out a user everywhere, change a user's address (keeps password, mail, and quota; open sessions stay signed in), remove user (revokes access immediately; the maildir stays on disk), set quota (MiB). Signed-in users change their own password on the Account page (current password required). Passwords must be at least 8 characters (length only, no composition rules). Mutations require a session cookie and a same-origin check when Origin/Referer is present. The running process reloads the users/quotas map live (no full restart).
From an SSH session you can do everything the /dns web page does — set the domain, generate a DKIM key, and enable HTTPS via Let's Encrypt — by editing config.toml in place (same atomic writers as the UI).
Not sure where you are? Run bare desertemail setup first: it prints a checklist of what is already configured (domain, users, DKIM, HTTPS) and the exact commands still needed — with your --config path filled in — in order.
desertemail setup -c /etc/desertemail/config.toml # status + guided next steps
desertemail setup domain example.com --host mail.example.com -c /etc/desertemail/config.toml
desertemail setup dkim -c /etc/desertemail/config.toml
desertemail setup https mail.example.com --email you@example.com -c /etc/desertemail/config.toml
# probe only: … setup https mail.example.com --email you@example.com --check-only
# force write: … setup https mail.example.com --email you@example.com --yes
domains and optional public_host.dkim.pem next to config unless already configured), chmod 600, updates dkim_selector / dkim_key_file, prints the TXT to publish. Refuses to overwrite an existing key unless --force (you must re-publish the TXT). Requires the openssl CLI — production keys (DKIM, ACME) are never silently generated by in-repo crypto; without openssl you get a clear error and can generate the key elsewhere instead, or knowingly opt in to the unaudited built-in generator with DESERTEMAIL_ALLOW_UNAUDITED_KEYGEN=1.acme=true, contact email, cert/key paths, web_tls_listen (default 0.0.0.0:8443 when empty), and public_url. The CLI does not start the ACME thread — restart desertemail so the server requests the certificate at startup.When you do not want to choose a password for someone, use Invite user on Admin. Enter user@ one of your configured domains; the server stores a one-time token hash under <data_dir>/invites.json (7-day expiry) and shows a copyable /invite?token=… link once. Hand the link over any channel, or optionally email it to an external address they already read — not their new mailbox (they cannot log in until they set a password). They open the branded invite page, choose a password (≥8 characters), and land in their inbox. Pending invites can be regenerated (new link) or revoked; used links cannot be reused.
One-click web backup (admin): on the Admin page, use Download backup. That issues GET /admin/backup (session required; no state mutation) and returns a single uncompressed POSIX ustar file named desertemail-backup-<domain>-<YYYYMMDD-HHMM>.tar. Contents:
desertemail-backup/config.tomldesertemail-backup/extras/ — DKIM key and TLS cert/key basenames when configureddesertemail-backup/data/… — full data dir (maildirs including .Junk/.Trash/.Sent/.Drafts, queue, invites, greylist), excluding maildir tmp/The download is built in memory and refused above ~512 MiB (use the shell script for huge mailboxes).
Restore on a new host:
desertemail --restore desertemail-backup-example.com-20260711-1200.tar \
--config /etc/desertemail/config.toml
# overwrites existing config / non-empty data dir only with --force
desertemail --config /etc/desertemail/config.toml
Restore extracts config + extras next to the target config path and data under <config_dir>/data, rewriting data_dir in the restored config to that location.
Large installs / rsync:
./deploy/backup.sh /var/lib/desertemail /var/backups/desertemail
CONFIG=/etc/desertemail/config.toml DKIM=/etc/desertemail/dkim.pem \
TLS_CERT=/etc/desertemail/tls.crt TLS_KEY=/etc/desertemail/tls.key \
./deploy/backup.sh /var/lib/desertemail /var/backups/desertemail
Atomicity: Maildir is safe to rsync while the server runs (you may miss a message mid-write). For a perfectly consistent snapshot, stop the unit first. The script header documents rsync restore steps.
GET /healthz — no auth; returns 200 and body ok (liveness).GET /metrics — Prometheus text format (counters + queue depth gauge). Optionally gated by metrics_token.# prometheus.yml
scrape_configs:
- job_name: desertemail
static_configs:
- targets: ["mail.example.com:8080"]
# authorization:
# credentials: "change-me"
Grafana: import a simple dashboard on the desertemail_* metrics (connections, auth success/fail, messages received/delivered/queued/bounced, greylist/spam rejects, queue depth). Alert on rising auth_failures, non-zero queue depth growth, or missing scrapes.
Use log_format = "json" (or text with structured fields). Auth failures emit event=auth_fail with ip, user, proto. Ship deploy/fail2ban-desertemail.conf and deploy/fail2ban-jail-desertemail.local — see also Structured logs & fail2ban.
log_format = "text" (default) — human-readable lines with level tags.log_format = "json" — one JSON object per line: ts, level, msg, plus fields such as event, ip, user, proto.Unit file: deploy/desertemail.service. SIGTERM/SIGINT triggers graceful shutdown (listeners stop accepting; in-flight connections finish briefly; queue is durable on disk).
sudo systemctl reload-or-restart desertemail # or: systemctl restart
# After config edits that are NOT users/quotas (listen addrs, TLS paths, domains), restart:
sudo systemctl restart desertemail
# User/password/quota changes via CLI while the server is stopped take effect on next start;
# admin UI applies users/quotas live without restart.
After first-run setup (or anytime as admin), open http://127.0.0.1:8080/dns. The page shows exactly which MX, A, SPF, DKIM, and DMARC records to publish at your registrar, with copy buttons. Generate a DKIM key in-browser (writes dkim.pem next to your config), then click Check DNS to verify live lookups. DesertEmail cannot create records at Cloudflare/Namecheap for you — it prepares and verifies them.
Installer summary points here too: Configure DNS in your browser: http://127.0.0.1:8080/dns. CLI: desertemail setup domain|dkim|https (see Domain & HTTPS setup), desertemail --dkim-dns example.com, and desertemail doctor.
mail.example.com) → public IP (DynDNS is fine for home).example.com → mail.example.com (priority 10 is typical).v=spf1 mx ~all on the apex (starter from the UI). Required for deliverability; DesertEmail also checks SPF on inbound mail./dns, or CLI:
desertemail --dkim-dns example.com --config ~/.desertemail/config.toml
Publish the TXT at <selector>._domainkey.example.com. Inbound DKIM signatures are verified on arrival.
_dmarc.example.com, start with
v=DMARC1; p=none; rua=mailto:admin@example.com, then tighten to p=quarantine / p=reject after reports look clean.
Inbound DMARC is evaluated; enforcement is opt-in via dmarc_enforce.mail.example.com). Many receivers require PTR that forward-confirms (FCrDNS). Home ISPs rarely allow useful PTR._mta-sts.example.com:
v=STSv1; id=YYYYMMDD01. Also publish a policy file at
https://mta-sts.example.com/.well-known/mta-sts.txt.
DesertEmail does not serve that file — put it on the webmail HTTPS host or any static HTTPS site._smtp._tls.example.com:
v=TLSRPTv1; rua=mailto:tlsrpt@example.com.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.
desertemail doctor automates most of the spam checklist above — run it after publishing DNS and before go-live. See Readiness check (doctor).
The installer configures the software. desertemail doctor probes the environment against the outside world so mail actually flows. Exit code = number of blockers (Fail checks); 0 means ready (warnings are allowed).
desertemail doctor
desertemail doctor --domain example.com --host mail.example.com
desertemail doctor --public-ip 203.0.113.10 --json
desertemail doctor --no-net # DNS-only (skip TCP probes)
| Group | Checks |
|---|---|
| Config | domains list; plaintext vs hashed passwords; factory default_password / allow_default_password_auth; require_tls_for_auth |
| DNS | MX; A/AAAA of mail host; SPF; DKIM published p= vs local key; DMARC; rDNS / FCrDNS; IP detection notes |
| Network | outbound port 25 (to a real MX); inbound :25 / :587 / :143 banners; port 80 when ACME or no TLS files yet (HTTP-01). Skipped with --no-net |
| TLS | cert load; expiry; SAN/CN covers mail host (and configured domains when present) |
Headline check — DKIM match: doctor compares the TXT at <selector>._domainkey.<domain> to the public key from dkim_key_file. A p= mismatch is the #1 silent deliverability failure (record published, but not the key you sign with).
PTR / rDNS is set in your hosting provider’s control panel (DigitalOcean, Hetzner, AWS, etc.) — not at your domain registrar. Doctor’s fix lines call this out explicitly.
DesertEmail doctor — deployment readiness
host=mail.example.com public_ip=203.0.113.10 egress=203.0.113.10
── DNS ──
✓ MX example.com — top=mail.example.com pref=10; A includes 203.0.113.10
✓ SPF example.com — v=spf1 mx a -all (policy -all (hard fail))
✗ DKIM example.com (s=mail) — p= mismatch at mail._domainkey.example.com
→ fix: Update TXT at mail._domainkey.example.com to exactly:
⚠ DMARC example.com — no v=DMARC1 TXT at _dmarc.example.com
→ fix: Publish TXT at _dmarc.example.com: v=DMARC1; p=none; ...
── Network ──
✓ outbound port 25 — connected to 142.251.x.x:25 (via gmail-smtp-in.l.google.com)
── TLS ──
⚠ TLS certificate — plaintext only — fine for LAN, not for public internet
VERDICT: 1 blocker(s), 2 warning(s)
Not ready: fix the red items
| Flag | Meaning |
|---|---|
--config / -c | Config path (same global flag as the server; default config.toml) |
--domain <d> | Domain(s) to check (repeatable); default = all domains from config |
--host <name> | Public mail hostname (default: MX target, else first domain) |
--public-ip <ip> | Expected public IP override (needed for PTR + inbound port probes when auto-detect fails) |
--json | Machine-readable JSON array of checks |
--no-net | DNS-only — skip TCP reachability probes |
Human output is green ✓ / yellow ⚠ / red ✗ on a TTY (or ok / warn / FAIL when not a terminal or NO_COLOR is set). Failed and warned lines may include → fix: hints. Final line: VERDICT: N blocker(s), M warning(s).
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 can supply a certificate and private key yourself, or enable built-in ACME (acme = true) for Let's Encrypt HTTP-01 (requires port 80 / web_listen reachable; see ACME section above).
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"
With acme = true, DesertEmail renews in the background when the cert has <30 days left (checks every 12h). For BYO certs (certbot/acme.sh), restart DesertEmail after the files on disk change — there is no config hot-reload.
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 overwriteFirst prompt: Press Enter for recommended settings, or type custom for advanced setup.
localhost, user admin, random password (printed at the end), default data dir, webmail on, high ports, DKIM off. No further questions.http://127.0.0.1:8080, opens a browser when possible.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; shown in summary when generated) |
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) |
DESERTEMAIL_AUTOSTART | 1/0 — start server after install (default 0 when non-interactive so CI does not hang; interactive defaults to yes) |
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 a full self-hostable mail server for personal and small-team use. The remaining gate before large-scale or high-stakes deployment is an external security audit plus a sustained fuzzing campaign of the hand-rolled parsers. 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.acme = true, HTTP-01; needs public DNS + port 80) — or bring your own certificate (self-signed for testing, certbot/acme.sh externally, or a TLS-terminating reverse proxy). See TLS / encryption setup.desertemail --hash-password or user add). Plaintext still works but logs a startup WARNING. Keep config.toml chmod 600.catch_all only routes mail; authentication requires a [users] entry unless allow_default_password_auth = true (keep false).HttpOnly, SameSite=Lax; on HTTPS (web_tls_listen) the cookie also gets Secure. Admin POSTs check Origin/Referer when present. Tokens are derived from OS CSPRNG material.dkimpy in project tests.If you need a high-volume mail gateway with decades of audit history, 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. Certs can be auto-issued and renewed via built-in ACME (acme = true), or 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).desertemail doctor (optionally --domain yourdomain.com). It probes DNS (MX/SPF/DKIM-match/DMARC/rDNS), ports, TLS, and config, prints a green/red report with → fix: lines, and exits with the number of blockers. See Readiness check (doctor)./bin/. The source is public on github.com/bitfent/desertemail.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