Burngate
Lightweight SMTP gateway that rejects spam at RCPT TO — before the message body is ever transmitted. Redis-backed, ~5 MB, single binary.
// How Burngate works
Internet ──▶ :25 burngate ──▶ Redis lookup
┌── not found ──▶ 550 reject (no body transferred)
└── found ──▶ accept DATA ──▶ relay to backend :2525
The problem
Most spam targets addresses that were never created. The typical SMTP server accepts the full email body, then checks the database — and discards it. You spent CPU, memory, and bandwidth for nothing.
The solution
SMTP announces the recipient at RCPT TO, before the body. Burngate checks Redis at that moment and rejects immediately if the address doesn't exist. Spam never transmits a single byte of body content.
Features
Pre-DATA filtering
Rejects unknown recipients before any body transfer
Redis-backed
Sub-millisecond mailbox existence checks
Configurable schema
Bring your own key pattern (mb:{address}, user:{address}:active, …)
Multi-domain
Multiple accepted domains with wildcard subdomain support
STARTTLS
Optional TLS upgrade via rustls
OpenTelemetry
OTLP traces with W3C traceparent propagation into relayed emails
Structured logging
JSON logs via tracing, configurable with RUST_LOG
Tiny footprint
Single static binary, ~5 MB, minimal memory
Async
Built on Tokio, handles thousands of concurrent connections
Docker-ready
Works as a sidecar in front of any backend mail server
Quick start
// Docker
docker run -d \ --name burngate \ -p 25:25 \ -e ACCEPTED_DOMAINS=example.com \ -e REDIS_HOST=your-redis-host \ -e BACKEND_SMTP=your-backend:2525 \ tempyemail/burngate:latest
Configuration
All configuration is via environment variables.
| Variable | Default | Description |
|---|---|---|
| ACCEPTED_DOMAINS | — | Required. Comma-separated list of accepted domains |
| BACKEND_SMTP | 127.0.0.1:2525 | Backend SMTP server to relay accepted mail to |
| REDIS_HOST | 127.0.0.1 | Redis hostname |
| REDIS_KEY_PATTERN | mb:{address} | Key pattern for mailbox lookup |
| REDIS_CHECK_MODE | both | key, set, or both |
| LISTEN_ADDR | 0.0.0.0:25 | Address and port to listen on |
| MAX_MESSAGE_SIZE | 10485760 | Maximum message size in bytes |
| CONNECTION_TIMEOUT | 300 | Connection timeout in seconds |
| TLS_CERT_PATH | — | PEM certificate path for STARTTLS |
| TLS_KEY_PATH | — | PEM private key path for STARTTLS |
| RUST_LOG | info | Log level: trace, debug, info, warn, error |
| OTEL_EXPORTER_OTLP_ENDPOINT | — | OTLP gRPC endpoint for trace export. Unset = OTel disabled |
| OTEL_SERVICE_NAME | burngate | Service name in traces |
OpenTelemetry tracing
Set OTEL_EXPORTER_OTLP_ENDPOINT to export traces via OTLP. Each SMTP session becomes a root span with the relay step as a child. A W3C traceparent header is injected into every relayed email so your downstream mail processor can attach its own spans to the same trace. Zero overhead when unset.
.NET Aspire dashboard
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:15901
OTEL_SERVICE_NAME=burngate
Any OTLP backend
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_SERVICE_NAME=burngate
// Trace hierarchy
smtp.session (peer=1.2.3.4)
└── smtp.relay (sender=..., size=...)
[traceparent injected into email headers]
↓ downstream mail processor continues trace