self-hosted · open source · Rust + Python

A drop-in anti-spam gateway for your mail server.

Postarmor sits in front of Postfix, Exchange, or any SMTP backend. It scans with ClamAV, runs a cascaded FastText → transformer → LLM classifier, learns from operator feedback, and quarantines threats — all without touching your existing mail server configuration.

$ sudo ./scripts/prepare_debian.sh
$ make setup && make up && make migrate
$ open http://localhost

What it does

Inbound mail hits a small Rust SMTP binary (~5 MB RSS) that runs cheap rejection gates first — RBL, rDNS, SPF, header and attachment policy, SURBL/URIBL URL reputation, VIP impersonation detection, a content-hash blocklist, and a fuzzy SimHash blocklist — before calling a FastText classifier sidecar on the hot path. Messages that survive the gates get DKIM / DMARC verification, then hand off to an async worker for ClamAV scanning and LLM analysis.

The LLM tier runs against a local Ollama model by default, or the Anthropic Claude API when you want stronger reasoning without giving up self-hosting. Results are cached by content hash; operator labels retrain the FastText model in-place with no container restart.

Everything runs in Docker Compose: SMTP · API · worker · classifier · ClamAV · Valkey · PostgreSQL · nginx + React dashboard. Point your MX records at one machine and you have a full filtering tier in under ten minutes.

Features

Rust SMTP frontend

tokio-based server on ports 25 / 587, ~5 MB idle RSS, full ESMTP state machine, STARTTLS, and a DATA-phase pipeline ordered cheapest check first so expensive crypto runs last.

DKIM / DMARC / ARC

RFC 6376 DKIM verify, RFC 7489 DMARC evaluate (off · tag · reject), RFC 8617 ARC sealing for downstream forwarders. All wrapped in a 5 s timeout so slow DNS never stalls the hot path.

Cascade classifier

FastText (<5 ms) then a CPU transformer if FastText is unsure. Confident verdicts skip the LLM entirely. Borderline messages escalate to Ollama or the Anthropic Claude API.

Content-hash & SimHash blocklists

Operator spam labels populate Valkey keyspaces that the SMTP server reads during DATA. Exact-hash blocks byte-identical resends; fuzzy SimHash catches near-duplicate mutations with Hamming-distance match.

ClamAV antivirus

INSTREAM TCP scan of HTML bodies and attachments. Virus detection quarantines the message and records the signature name in the dashboard.

LLM analysis with context

SPF, DKIM, and DMARC verdicts are injected into the LLM prompt via trusted Authentication-Results and X-Gateway-* headers. Results are cached by content hash so spam blasts skip the LLM.

Operator feedback loop

Release-from-quarantine and mark-as-spam write labeled training rows. When the LLM confidently disagrees with the fast stages, the worker auto-labels the row. Admins retrain FastText on demand from the dashboard — model swaps atomically, no restart.

Per-IP rate limiting

Four independent throttles per source IP: concurrent session cap, new-connection rate, accepted-message rate, adaptive auto-blocker driven by a sliding-window rejection counter. All state in Valkey, fail-open on errors.

Delivery retry (RFC 5321)

Backend 4xx or connection failure queues the message with exponential backoff (5 min → 30 min → 2 h → 6 h → 24 h → 48 h → 96 h). Only hard 5xx bypasses the retry queue.

React dashboard

Live metrics, email log with per-stage verdict chip, quarantine with bulk release, feedback & retrain controls, rate-limit editor, SMTP debug trace viewer. TanStack Query keeps everything near-real-time.

SMTP debug trace

Runtime-toggleable per-session protocol trace captured into smtp_debug_logs. Every accepted, rejected, or aborted connection recorded in full when enabled — zero overhead when off.

Self-hosted & auditable

Runs entirely on your hardware. No SaaS dependency, no third-party analytics, no phone-home. Full source on GitHub — Rust for the SMTP / worker binaries, Python for the FastAPI control plane and classifier, React for the dashboard.

How it works

One sending MTA talks to one Rust binary. Everything else hangs off Valkey and PostgreSQL.


  Sending MTA
      |
      v
  +------------------------------------------------------------+
  |  smtp_rust     DATA-phase pipeline (cheapest first)        |
  |                                                            |
  |  rate-limit -> RBL/SPF -> header/attachment checks ->      |
  |    content-hash -> SimHash -> FastText inline ->           |
  |      DKIM / DMARC verify -> stamp A-R -> insert            |
  +------------------------------------------------------------+
         |                                     |
         v                                     v
    PostgreSQL <----- worker_rust  --------> Valkey
                         |
            +------------+-----------+-----------+
            v            v           v           v
         ClamAV      classifier    Ollama     Claude
        antivirus   FastText +    (local)     (API)
                    transformer

Inbound mail gets rejected at the earliest stage that can decide — a spam-blast hit on the content-hash or SimHash blocklist never pays the DKIM DNS lookup cost. Messages that survive the DATA pipeline land in PostgreSQL, get enqueued in Valkey, and are picked up by the worker binary for antivirus scanning and cascade classification. Confident FastText or transformer verdicts write a final decision directly; borderline cases escalate to the LLM tier.

Quick start

Postarmor runs on Linux, macOS, or anything that runs Docker. On a fresh Debian 12 / 13 host the bootstrap script installs every dependency in one shot — Docker Engine, Node.js 22, Rust stable, Python 3, and the base C build toolchain.

1. Bootstrap the host (Debian 12 / 13 only)

sudo ./scripts/prepare_debian.sh            # install everything
sudo ./scripts/prepare_debian.sh --check    # verify-only
newgrp docker                               # activate group membership

2. Configure

git clone https://github.com/ahmetbuba/postarmor.git
cd postarmor
make setup          # copies .env.example -> .env
$EDITOR .env        # set DB password, backend SMTP host, admin password

3. Start the stack

make up             # builds all images and starts services (~5 min first run)
make migrate        # creates the database schema
make pull-model     # downloads llama3.2:3b (~2 GB, one-time)

4. Open the dashboard

open http://localhost
# login with ADMIN_USERNAME / ADMIN_INITIAL_PASSWORD from .env

Point your MX records (or inbound SMTP relay) at this server on port 25. Set BACKEND_SMTP_HOST or configure domain routes in the dashboard to send cleaned mail onward to your real backend.

Documentation & links