Docs

What gets installed, how the server is wired, every config key, and honest security notes — so you know exactly what you are running.

▶ On this page

What is DesertEmail

#

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:

  • Self-hosters who want a readable, rsync-friendly Maildir stack
  • Tiny hardware: Pi Zero, 128 MB VPS, old netbooks, Termux on a phone
  • Learning email protocols by reading a small codebase

It is deliberately minimal. See Security notes before putting it on the public internet.

What gets installed on your machine

#

Default prefix is ~/.desertemail (override with DESERTEMAIL_PREFIX). On Windows: %USERPROFILE%\.desertemail.

  • Binary$PREFIX/bin/desertemail (Windows: desertemail.exe)
  • Config$PREFIX/config.toml (mode 0600 when the OS allows)
  • Optional DKIM key$PREFIX/dkim.pem if you enable DKIM during the wizard
  • Mail data — Maildirs under $PREFIX/data by default (or DESERTEMAIL_DATA_DIR)
  • PATH block — one block appended to your shell rc (zsh/bash/fish/.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).

  • Optional systemd unit/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.
  • Optional macOS launchd agent~/Library/LaunchAgents/org.desertemail.plist when you accept “Start DesertEmail now” (or DESERTEMAIL_AUTOSTART=1). Survives reboots via RunAtLoad.
  • Log file$PREFIX/desertemail.log when the installer (or launchd) starts the server

Safety guarantees of the installer:

  • Downloads from this site’s /bin/ and verifies SHA-256 against /bin/SHA256SUMS when present (mismatch aborts; missing sums file warns and continues)
  • Never runs sudo without a reason (systemd unit only) and, interactively, only after you ask for the unit
  • Re-runs never overwrite an existing config.toml without asking (non-interactive re-runs keep the existing config)

How to uninstall

#

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.

Manual steps (fallback)

#
  1. Stop the process: systemctl stop desertemail (Linux unit), launchctl bootout gui/$(id -u)/org.desertemail (macOS agent), or kill the background process.
  2. Delete the prefix directory: rm -rf ~/.desertemail (or your DESERTEMAIL_PREFIX). Keep data/ if you want to preserve mail.
  3. Remove the PATH block between the markers in your shell rc (or remove the user PATH entry on Windows).
  4. If installed: sudo systemctl disable --now desertemail and sudo rm /etc/systemd/system/desertemail.service, then sudo systemctl daemon-reload.
  5. On macOS, also remove ~/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.

Architecture

#

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:

  1. SMTP inbound — default high port 0.0.0.0:2525 (real-world: 25); STARTTLS (RFC 3207) advertised when TLS is configured
  2. SMTP submission — authenticated clients, default 0.0.0.0:2587 (real: 587); STARTTLS when TLS is configured
  3. IMAP — default 0.0.0.0:2143 (real: 143); STARTTLS (RFC 2595) when TLS is configured
  4. Webmail + admin — HTTP/1.1 on 0.0.0.0:8080 (empty web_listen disables it)
  5. Optional implicit TLS — SMTPS (smtps_listen, e.g. 465), IMAPS (imaps_listen, e.g. 993), HTTPS webmail (web_tls_listen, e.g. 8443; Secure session cookies)
  6. Outbound queue worker — disk queue under 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).

Configuration reference

#

Config is a hand-parsed TOML-like file (key = "value", simple lists, [users] section). Defaults below match src/config.rs Default.

Keys

#
KeyTypeDefaultMeaning
domainslist of strings["localhost"]Domains this server accepts mail for
data_dirstring"./data"Maildir + queue root (absolute path recommended)
smtp_listenstring"0.0.0.0:2525"Inbound SMTP bind address
submission_listenstring"0.0.0.0:2587"Authenticated submission bind
imap_listenstring"0.0.0.0:2143"IMAP bind address
web_listenstring"0.0.0.0:8080"Webmail/admin HTTP bind; empty string disables
admin_userstring / unsetunsetLogin name allowed on /admin; empty/unset disables admin
smarthostoptional stringnoneRelay host:port when direct MX/port 25 is unavailable
smarthost_useroptional stringnoneAUTH user for smarthost
smarthost_passoptional stringnoneAUTH password for smarthost
catch_allbooltrueAccept any local-part@domain and auto-create mailbox
default_passwordstring"changeme"Password for catch-all / auto-created mailboxes
dkim_selectorstring"mail"DKIM selector; DNS name is <selector>._domainkey.<domain>
dkim_key_fileoptional stringnonePath to PEM RSA private key for signing
tls_cert_fileoptional stringnonePEM certificate chain; both this and tls_key_file required to enable TLS
tls_key_fileoptional stringnonePEM private key (PKCS#8 or RSA)
smtps_listenstring"" (disabled)Implicit SMTPS bind (e.g. "0.0.0.0:465"); only bound when TLS cert/key loaded and non-empty
imaps_listenstring"" (disabled)Implicit IMAPS bind (e.g. "0.0.0.0:993"); only bound when TLS loaded and non-empty
web_tls_listenstring"" (disabled)HTTPS webmail bind (e.g. "0.0.0.0:8443"); Secure session cookies when used
require_tls_for_authboolfalseIf true, reject SMTP AUTH on plaintext with 538
spf_enforceboolfalseWhen true, SPF hard-Fail + DMARC reject policy may 550; otherwise annotate only
dmarc_enforceboolfalseWhen true, honor DMARC p=reject (550) / p=quarantine (tag); default annotate only
greylistboolfalseInbound greylisting: first triplet sight → 451
greylist_delay_secsu6460Minimum wait before greylist retry is accepted
greylist_ttl_secsu642592000Whitelist TTL after successful retry (30 days)
dnsblslist[]DNSBL zones (e.g. zen.spamhaus.org)
dnsbl_rejectboolfalseWhen true, a DNSBL hit alone causes 550
spam_score_tagi325Score ≥ this adds X-Spam-Flag: YES
spam_score_rejecti320Score ≥ this → 550; 0 disables reject
spam_folder_thresholdi324Score ≥ this delivers to Spam (.Junk); 0 disables
spam_check_ptrbooltrueInclude missing/mismatched rDNS in spam score
default_quota_mbu640Default mailbox quota in MiB; 0 = unlimited. Over-quota: SMTP 452 4.2.2, IMAP APPEND NO [OVERQUOTA]
log_formatstring"text""text" or "json" (one object per line: ts, level, msg, fields). Auth failures: event=auth_fail
acmeboolfalseEnable ACME v2 auto TLS (Let's Encrypt). Non-blocking background thread
acme_emailstring""Account contact (required when acme=true)
acme_directorystringLE productionACME directory URL; use staging for tests
acme_domainslistdomainsHostnames on the certificate
max_message_bytesu6426214400 (25 MiB)Max SMTP DATA / IMAP APPEND size; oversize → SMTP 552 / IMAP NO
metrics_tokenstring""If non-empty, GET /metrics requires Authorization: Bearer … or ?token=
[users]mapempty"local" or "user@domain" = password or PBKDF2 hash
[quotas]mapemptyPer-user quota overrides in MiB (e.g. "alice" = 512)

IMAP capabilities

#

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.

ACME / Let's Encrypt

#

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.

Structured logs & fail2ban

#

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.

Annotated example

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

Operations

#

Day-2 ops: add users without hand-editing config, back up Maildirs, scrape health/metrics, fail2ban, and restart under systemd.

User management (CLI + admin UI)

#

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

Domain & HTTPS setup (CLI)

#

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
  • setup domain — writes domains and optional public_host.
  • setup dkim — generates a 2048-bit RSA key (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.
  • setup https — normalizes the domain, runs A/AAAA + port-80 checks (same as the web UI), then writes 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.

Inviting users

#

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.

Backup & migrate

#

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.toml
  • desertemail-backup/extras/ — DKIM key and TLS cert/key basenames when configured
  • desertemail-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.

Health & metrics

#
  • 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.

fail2ban

#

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 formats

#
  • 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.

Graceful restart under systemd

#

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.

DNS setup

#

DNS via the web UI

#

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.

  1. A / AAAA — hostname of your mail server (e.g. mail.example.com) → public IP (DynDNS is fine for home).
  2. MXexample.commail.example.com (priority 10 is typical).
  3. SPF (TXT) — e.g. v=spf1 mx ~all on the apex (starter from the UI). Required for deliverability; DesertEmail also checks SPF on inbound mail.
  4. DKIM (TXT) — use Generate DKIM key on /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.
  5. DMARC (TXT) — at _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.
  6. rDNS / PTR — ask your host/VPS to set reverse DNS for your public IP to your mail hostname (e.g. mail.example.com). Many receivers require PTR that forward-confirms (FCrDNS). Home ISPs rarely allow useful PTR.
  7. MTA-STS (optional) — TXT at _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.
  8. TLS-RPT (optional) — TXT at _smtp._tls.example.com: v=TLSRPTv1; rua=mailto:tlsrpt@example.com.

Why my mail goes to spam

#
  • SPF + DKIM + DMARC published and aligned on the same organizational domain
  • PTR/rDNS set to your mail hostname and forward-confirms
  • Not sending from a residential or previously abused IP (use a VPS or smarthost)
  • TLS on submission; hostname matches cert when possible
  • Warm a new IP slowly; avoid bulk/spammy content
  • Check major DNSBLs before go-live

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

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)

What it checks

#
GroupChecks
Configdomains list; plaintext vs hashed passwords; factory default_password / allow_default_password_auth; require_tls_for_auth
DNSMX; A/AAAA of mail host; SPF; DKIM published p= vs local key; DMARC; rDNS / FCrDNS; IP detection notes
Networkoutbound 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
TLScert 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.

Sample output

#
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

Flags

#
FlagMeaning
--config / -cConfig 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)
--jsonMachine-readable JSON array of checks
--no-netDNS-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 / encryption setup

#

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

Quick self-signed cert (testing)

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

Which ports do what

#
ListenerTypical portMode
smtp_listen25 (or 2525)Plaintext; advertises STARTTLS (RFC 3207) when TLS configured
submission_listen587 (or 2587)Same — STARTTLS upgrade; AUTH required for submission
imap_listen143 (or 2143)Plaintext; advertises STARTTLS (RFC 2595) when TLS configured
smtps_listen465Implicit TLS before banner (submission semantics); only if non-empty + TLS loaded
imaps_listen993Implicit TLS before IMAP greeting
web_listen8080HTTP webmail (no TLS on this socket)
web_tls_listen8443HTTPS 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).

Real certificates (Let’s Encrypt via external tools)

#

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.

Force encrypted logins

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

What still isn’t covered

#
  • ACME is optional and needs public HTTP-01 (port 80); without it, bring your own cert
  • Outbound TLS is opportunistic — remote STARTTLS with full cert validation; on failure the queue reconnects plaintext so delivery is not blocked
  • Passwords remain plaintext in config.toml; TLS only protects the wire
  • You may still put a reverse proxy in front for HTTP(S) if you prefer terminating TLS elsewhere

The installers

#

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

What each run does

#
  1. Download the binary for the fixed target triple baked into that script
  2. Verify SHA-256 against /bin/SHA256SUMS when available
  3. Install under $PREFIX/bin and append the PATH block (idempotent markers)
  4. Express config by default (or advanced wizard / env defaults in non-interactive mode)
  5. Write config.toml if missing / if you confirm overwrite
  6. Optionally install systemd unit (Linux) or launchd agent (macOS when starting)
  7. Optionally start the server, wait for webmail, and open the browser
  8. Print login summary (password shown in express mode) and DNS/DKIM hints

Wizard (express by default)

#

First prompt: Press Enter for recommended settings, or type custom for advanced setup.

  • Express (default) — domain localhost, user admin, random password (printed at the end), default data dir, webmail on, high ports, DKIM off. No further questions.
  • Custom — primary domain, admin user, password (hidden; Enter generates), data directory, webmail Y/N, port set high/privileged, DKIM Y/N, overwrite existing config if present, optional systemd unit (Linux)
  • Start now? — after config, “Start DesertEmail now and open webmail?” (default yes). Starts in the background (macOS: launchd user agent; Linux: nohup, or systemd if you opted in), waits for http://127.0.0.1:8080, opens a browser when possible.

Env vars (headless / CI)

#

Use DESERTEMAIL_NONINTERACTIVE=1 with optional overrides:

VariablePurpose
DESERTEMAIL_NONINTERACTIVE=1Skip prompts; use defaults / other env vars
DESERTEMAIL_PREFIXInstall root (default ~/.desertemail)
DESERTEMAIL_DOMAINPrimary domain (default localhost)
DESERTEMAIL_ADMIN_USERAdmin / first user (default admin)
DESERTEMAIL_ADMIN_PASSWORDAdmin password (else random; shown in summary when generated)
DESERTEMAIL_DATA_DIRMaildir root (default $PREFIX/data)
DESERTEMAIL_WEBMAIL1/0 — enable web UI (default on)
DESERTEMAIL_PORTShigh or privileged
DESERTEMAIL_DKIM1/0 — generate/use DKIM key
DESERTEMAIL_SYSTEMD1/0 — install unit (POSIX only; Windows has no systemd)
DESERTEMAIL_AUTOSTART1/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

Platforms, Windows, Termux, source

#
  • Linux x86_64 / arm64 / armv7 / armv6 (musl static) — curl … | sh
  • macOS Apple Silicon & Intel — same curl form
  • Windowsirm https://YOUR-SITE/install-windows.ps1 | iex (or Bypass if execution policy blocks). Installs desertemail.exe, verifies with Get-FileHash, user PATH entry
  • Android (Termux) — use the Linux ARM64 installer after pkg install curl openssl (F-Droid Termux). High ports need no root
  • Build from source — needs git + Rust toolchain (cargo build --release); use when no prebuilt matches your machine

Security notes & current limitations

#

DesertEmail 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 is built in (rustls): STARTTLS on SMTP (RFC 3207) and IMAP (RFC 2595), optional implicit SMTPS/IMAPS, HTTPS webmail, and opportunistic outbound STARTTLS with webpki-roots validation. Supply 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.
  • Automatic certs via built-in ACME (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.
  • Passwords: prefer PBKDF2 hashes (desertemail --hash-password or user add). Plaintext still works but logs a startup WARNING. Keep config.toml chmod 600.
  • Catch-all vs auth: catch_all only routes mail; authentication requires a [users] entry unless allow_default_password_auth = true (keep false).
  • Webmail sessions: cookie is 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.
  • DKIM: pure-std RSA-SHA256, relaxed/relaxed; signing path verified against dkimpy in project tests.
  • Missing for “real MTA” use: no full Bayesian/ML spam filter (basic SPF/DKIM/DMARC + greylist/DNSBL score only); ACME is optional and needs correct DNS + port 80.

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.

FAQ

#
Does it support TLS / encryption?
Yes — built in via rustls. Set 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.
Will Gmail accept my mail?
Maybe. SPF + DKIM + sensible rDNS help a lot. New or residential IPs still have low reputation; expect spam folders or rejects until reputation builds — or send via a smarthost with good standing.
Can I run it on my phone?
Yes on Termux (Android): Linux ARM64 musl binary, high ports, no root required for that port set.
My ISP blocks port 25 — can I still send?
Set 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.
Multiple domains?
Yes — list them in domains = ["a.com", "b.com"]. Users can be local-parts or full addresses.
Where is mail stored?
Maildir trees under data_dir (default ~/.desertemail/data). Rsync-friendly; queue lives in data_dir/queue.
How do I update?
Re-run the same installer. Binary is replaced; existing config is kept unless you explicitly overwrite.
Is Windows supported?
Yes: MSVC .exe + PowerShell installer. Maildir uses NTFS-safe naming (!2, instead of :2, in filenames).
How do I know my server is set up correctly?
Run 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).
Does the installer call GitHub?
No. Binaries and checksums come from this site under /bin/. The source is public on github.com/bitfent/desertemail.
Is it still “zero dependencies”?
No. Mail/IMAP/webmail/DKIM/DNS/HTTP remain pure 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