Scalable Reliable Mail Transfer Agent
RFC 5321/5322 compliant MTA built in Go — designed for high-volume transactional and bulk email delivery.
SRMTA is an open-source mail transfer agent written in Go. It handles inbound and outbound SMTP with a focus on deliverability: per-provider throttling, MX-based IP segregation, DKIM signing, bounce classification, SPF validation, and a crash-safe queue. A Node.js admin API and a static dashboard ship alongside the core binary.
| Category | Capabilities |
|---|---|
| SMTP Engine | ESMTP, STARTTLS (TLS 1.2+), AUTH (PLAIN/LOGIN/CRAM-MD5), pipelining (RFC 2920), DSN, separate inbound :25 and submission :587 ports |
| Queue | 6 spools (incoming → active → deferred → retry → dead-letter → failed), domain bucketing, sharding, crash-safe WAL journal |
| DNS | Async resolver with in-memory caching, MX priority, A/AAAA fallback, resolver pool |
| IP Pool | Health scoring (4xx/5xx/timeout/TLS failures), auto-disable/recovery, warm-up schedules |
| Smart Throttle | Per-provider speed limits (Microsoft, Google, Yahoo, Apple, Zoho, Comcast), adaptive backoff on 4xx, rolling rate windows |
| MX-Based Routing | Provider → dedicated IP segregation (Google, Outlook, Yahoo, Zoho), primary → backup → fallback chains |
| Sender Routing | Bind specific FROM domains or wildcards (*.corp.com) to dedicated IPs or subnets |
| DKIM | Multi-key signing, relaxed canonicalization, per-domain key selection |
| Bounce | Hard/soft/block/policy/mailbox classification, auto-suppression, sender auto-pause |
| Access Control | INI-based allowed_ips.ini and allowed_domains.ini with CIDR ranges and wildcard domains |
| Compliance | FBL webhook ingestion, List-Unsubscribe (RFC 2369 + RFC 8058), SPF validation, DMARC alignment |
| Observability | Prometheus metrics endpoint, structured JSON logging, Grafana dashboard (grafana/dashboard.json) |
| Admin API | Node.js/Fastify REST API with JWT (30-min expiry), RBAC, per-endpoint rate limiting, WebSocket live updates |
| Config | Centralized YAML + config.d/*.yaml sub-configs, environment variable interpolation |
SRMTA is security-by-default. The application will not start without required secrets set:
| Requirement | Details |
|---|---|
JWT_SECRET |
Required. No default. Generate with openssl rand -hex 32 |
WEBHOOK_SECRET |
Required. Used for HMAC-SHA256 webhook verification with constant-time comparison |
DB_PASSWORD |
Required. No hardcoded defaults in source or config |
| CORS | Explicit origin allowlist via ALLOWED_ORIGINS (no wildcard) |
| Auth | Database-backed bcrypt authentication. No default credentials |
| SMTP Auth | Default validator rejects all credentials — a real AuthValidator must be injected |
| TLS | STARTTLS handshake failures abort the connection (no plaintext fallback). DB connections default to ssl_mode: require |
| WebSocket | Requires JWT authentication |
| Rate limiting | Global (100/min) + stricter per-endpoint (login: 10/min) |
| Message IDs | Generated with crypto/rand (not math/rand) |
| Bulk send | Capped at 1,000 messages per batch |
For vulnerability reporting, see SECURITY_AUDIT.md or open a GitHub Security Advisory.
Core MTA binary:
- Go 1.22+
- PostgreSQL 14+
Admin API (optional, runs separately):
- Node.js 20+
- Redis 6+ (used for rate limiting)
# Copy and fill in required secrets
cp deploy/systemd/srmta.env.example deploy/systemd/srmta.env
vim deploy/systemd/srmta.env # set JWT_SECRET, WEBHOOK_SECRET, DB_PASSWORD
# Build and start (MTA + admin API + PostgreSQL + Redis)
docker compose up -d
# Tail logs
docker compose logs -f mtaThe MTA runs with network_mode: host so it can bind to port 25 and make outbound SMTP connections without NAT. PostgreSQL is initialized automatically from migrations/init_postgres.sql.
git clone https://github.com/rushikeshsakharleofficial/SRMTA.git
cd SRMTA
make build
make test
sudo make installmake rpm
sudo rpm -ivh rpmbuild/RPMS/x86_64/srmta-1.2.0-1.el9.x86_64.rpmmake deb
sudo dpkg -i debbuild/srmta_1.2.0-1_amd64.deb
sudo apt-get install -f# 1. Set required secrets (do this before anything else)
sudo vim /etc/srmta/srmta.env # JWT_SECRET, WEBHOOK_SECRET, DB_PASSWORD
# 2. Edit main config and sub-configs
sudo vim /etc/srmta/config.yaml
sudo vim /etc/srmta/config.d/10-smtp.yaml # SMTP ports & limits
sudo vim /etc/srmta/config.d/20-dkim.yaml # DKIM keys
sudo vim /etc/srmta/config.d/30-ips.yaml # IP pool
sudo vim /etc/srmta/config.d/40-database.yaml # DB credentials
sudo vim /etc/srmta/config.d/50-queue.yaml # Queue & retry policy
sudo vim /etc/srmta/config.d/60-throttle.yaml # Per-provider speed limits
sudo vim /etc/srmta/config.d/70-routing.yaml # MX-based IP routing
# 3. Edit access control lists
sudo vim /etc/srmta/allowed_domains.ini
sudo vim /etc/srmta/allowed_ips.ini
# 4. Initialize the database
sudo -u postgres createuser srmta
sudo -u postgres createdb -O srmta srmta
psql -U srmta -d srmta -f /usr/share/srmta/migrations/001_init.sql
# 5. Generate DKIM keys
openssl genrsa -out /etc/srmta/dkim/example.com.key 2048
openssl rsa -in /etc/srmta/dkim/example.com.key -pubout \
| grep -v "^---" | tr -d '\n'
# → Add the resulting public key as DNS TXT: default._domainkey.example.com
# 6. Start the service
sudo systemctl enable --now srmta.service
# 7. Verify
sudo systemctl status srmta
curl http://localhost:9090/metricsSRMTA uses a layered configuration system:
- Main config (
/etc/srmta/config.yaml) — base settings - Sub-configs (
/etc/srmta/config.d/*.yaml) — merged on top, sorted alphabetically - INI files —
allowed_domains.ini,allowed_ips.inifor access control - Environment variables — expand
${VAR}in any YAML value
See configs/config.example.yaml for a fully annotated reference.
# config.d/10-smtp.yaml
smtp:
inbound_addr: ":25" # Receiving (MX traffic)
submission_addr: ":587" # Authenticated client submission (MSA)
outbound_port: 25 # Remote port for outbound delivery# allowed_domains.ini
[domains]
example.com
.example.com ; wildcard — all subdomains
# allowed_ips.ini
[ipv4]
203.0.113.10
10.0.0.0/8
[ipv6]
2001:db8::1
2001:db8::/32Configured in config.d/60-throttle.yaml. Default limits that avoid blacklisting:
| Provider | Max conn | Per sec | Per min | Per hour | Backoff |
|---|---|---|---|---|---|
| Microsoft/Outlook | 5 | 3 | 100 | 2,000 | 3×, max 10 min |
| Google/Gmail | 10 | 10 | 300 | 5,000 | 2.5×, max 8 min |
| Yahoo/AOL | 5 | 5 | 150 | 3,000 | 2×, max 10 min |
| Apple/iCloud | 3 | 2 | 60 | 1,000 | 3×, max 15 min |
| Zoho | 5 | 5 | 150 | 3,000 | 2.5×, max 10 min |
| Comcast/Xfinity | 3 | 2 | 50 | 800 | 3×, max 15 min |
| Default (others) | 10 | 20 | 500 | 10,000 | 2×, max 5 min |
Rates automatically reduce on 4xx throttle responses and recover on success.
Recipient MX lookup
│
├── *.google.com → IPs: 203.0.113.10, .11 (backup: .20)
├── *.outlook.com → IPs: 203.0.113.12, .13 (backup: .21)
├── *.yahoodns.net → IPs: 203.0.113.14 (backup: .22)
├── *.mail.icloud.com → IPs: 203.0.113.15 (backup: .23)
├── *.zoho.com → IPs: 203.0.113.19 (backup: .24)
└── * (all others) → IPs: 203.0.113.16-.18 (fallback: .50-.51)
Configure in config.d/70-routing.yaml. Sender-domain routes (sender_routes) are evaluated first; unmatched mail falls through to MX-based routing.
The Node.js/Fastify API runs independently of the core MTA:
cd web/api
npm install
export JWT_SECRET=$(openssl rand -hex 32)
export WEBHOOK_SECRET=$(openssl rand -hex 32)
export DB_PASSWORD="your-db-password"
export REDIS_URL="redis://localhost:6379"
export ALLOWED_ORIGINS="https://your-admin-domain.com"
npm start # Listens on :3000Credentials are stored in the api_users table (bcrypt). All routes require JWT + RBAC (admin / operator / viewer).
| Endpoint | Auth | Description |
|---|---|---|
POST /api/auth/login |
public | Returns JWT (30-min expiry) |
GET /api/auth/me |
JWT | Current user info |
POST /api/send |
operator+ | Queue a single message |
POST /api/bulk |
operator+ | Queue up to 1,000 messages |
POST /api/schedule |
operator+ | Schedule a message |
GET /api/status/:id |
operator+ | Delivery event status |
GET /api/metrics |
operator+ | Aggregated delivery stats (last 24 h) |
GET /api/logs |
operator+ | Paginated delivery logs (max 200/page) |
GET /api/logs/export |
operator+ | CSV export (max 50,000 rows) |
GET /api/queue/stats |
JWT | Queue spool depths |
GET /api/ips |
JWT | IP pool health snapshots |
POST /api/webhook |
HMAC | FBL/delivery status webhook |
GET /ws |
JWT | WebSocket live metrics feed |
Full OpenAPI spec: web/api/openapi.yaml
Open web/ui/index.html in a browser (or serve via nginx/Caddy). Connects to the API at the same origin — no hardcoded URLs.
scrape_configs:
- job_name: srmta
static_configs:
- targets: ['mta-host:9090']Import grafana/dashboard.json — pre-built panels for delivery rate, queue depth, active connections, success ratio, IP health, bounces, and latency.
See docs/MONITORING.md for a full observability runbook.
┌──────────────┐ ┌────────────┐ ┌───────────────┐ ┌──────────────┐
│ Inbound │───▶│ Queue │───▶│ Throttle │───▶│ Delivery │
│ SMTP :25 │ │ Manager │ │ Manager │ │ Engine │
│ MSA :587 │ │ (6 spools)│ │ (per-provider│ │ (worker pool│
└──────────────┘ └────────────┘ │ speed ctrl) │ └──────┬───────┘
└───────────────┘ │
┌─────────────────────────┬──────────────┬─────┘
│ │ │
┌─────▼─────┐ ┌──────────┐ ┌─▼────────┐ ┌─▼──────────┐
│ DNS │ │ MX-Based │ │ DKIM │ │ Bounce │
│ Resolver │ │ IP Router│ │ Signer │ │ Classifier │
└────────────┘ └──────────┘ └──────────┘ └────────────┘
SRMTA/
├── cmd/
│ ├── srmta/ # Main MTA binary
│ │ └── main.go
│ └── srmtaq/ # Queue management CLI
│ └── main.go
│
├── internal/
│ ├── smtp/ # SMTP engine
│ │ ├── server.go # Listener, connection admit, graceful stop
│ │ ├── session.go # Per-connection state machine (EHLO→DATA)
│ │ ├── client.go # Outbound client with connection pooling
│ │ ├── pipeline.go # RFC 2920 pipelining
│ │ └── ratelimiter.go # Per-IP connection rate limiting
│ │
│ ├── queue/ # 6-spool queue
│ │ ├── manager.go # Enqueue, dispatch, domain sharding
│ │ ├── journal.go # Crash-safe write-ahead log
│ │ ├── retry.go # Retry backoff and classification
│ │ └── scheduler.go # Deferred and timed delivery
│ │
│ ├── delivery/ # Outbound worker pool
│ │ ├── engine.go # Concurrent delivery orchestration
│ │ └── circuit_breaker.go # Per-domain failure isolation
│ │
│ ├── routing/
│ │ └── router.go # MX-based and sender-domain IP selection
│ │
│ ├── throttle/
│ │ └── manager.go # Per-provider speed limits, adaptive backoff
│ │
│ ├── dns/
│ │ ├── resolver.go # Async resolver with in-memory cache
│ │ └── pool.go # Resolver pool for concurrency
│ │
│ ├── ip/
│ │ └── pool.go # Health scoring, warm-up, auto-disable/recovery
│ │
│ ├── dkim/
│ │ └── signer.go # Multi-key signing, per-domain key selection
│ │
│ ├── bounce/
│ │ └── classifier.go # Hard/soft/block/policy classification, suppression
│ │
│ ├── compliance/
│ │ ├── compliance.go # FBL, List-Unsubscribe, DMARC alignment
│ │ └── spf.go # SPF host check (RFC 7208)
│ │
│ ├── access/
│ │ └── access.go # IP/domain ACL (INI allowlists, CIDR)
│ │
│ ├── store/
│ │ └── store.go # PostgreSQL + MySQL delivery event store
│ │
│ ├── metrics/
│ │ └── prometheus.go # Counters, histograms, gauges
│ │
│ ├── logging/
│ │ └── events.go # Structured JSON logger with masking
│ │
│ └── config/
│ └── config.go # YAML loader, config.d merge, validation
│
├── configs/ # Config templates (copy to /etc/srmta/)
│ ├── config.example.yaml # Fully annotated main config
│ ├── config.d/
│ │ ├── 10-smtp.yaml.example # SMTP ports and limits
│ │ ├── 20-dkim.yaml.example # DKIM keys
│ │ ├── 30-ips.yaml.example # IP pool
│ │ ├── 40-database.yaml.example # DB credentials
│ │ ├── 50-queue.yaml.example # Queue and retry policy
│ │ ├── 60-throttle.yaml.example # Per-provider speed limits
│ │ ├── 70-routing.yaml.example # MX-based IP routing
│ │ └── 75-sender-routing.yaml.example # Sender-domain → IP binding
│ ├── allowed_domains.ini.example
│ └── allowed_ips.ini.example
│
├── migrations/
│ ├── init_postgres.sql
│ └── init_mysql.sql
│
├── deploy/
│ ├── systemd/ # srmta.service, srmta.socket, srmta.env.example
│ ├── rpm/ # srmta.spec
│ └── deb/ # debian/ packaging (control, changelog, rules)
│
├── web/
│ ├── api/ # Node.js / Fastify admin API
│ │ ├── src/server.js
│ │ ├── Dockerfile
│ │ ├── openapi.yaml
│ │ └── package.json
│ └── ui/
│ └── index.html # Static admin dashboard (no build step)
│
├── grafana/
│ └── dashboard.json # Pre-built 12-panel Grafana dashboard
│
├── Dockerfile # Multi-stage build: Go binary → alpine runtime
├── docker-compose.yml # MTA + admin API + PostgreSQL + Redis
└── docs/
├── MONITORING.md
├── PRODUCTION_CHECKLIST.md
├── SCALING.md
└── SECURITY_AUDIT.md
| Command | Description |
|---|---|
make build |
Compile ./srmta (CGO disabled, version embedded) |
make test |
Run all tests with race detector (go test -race ./...) |
make bench |
Run benchmarks with memory stats |
make lint |
Run golangci-lint |
make install |
Install binary, systemd units, config templates |
make rpm |
Build RPM package |
make deb |
Build Debian package |
make clean |
Remove build artifacts |
Contributions are welcome when they add real value. Low-effort PRs (trivial renames, comment tweaks, style-only changes with no substance) will be closed without review.
Before opening a PR:
- Open an issue first for anything non-trivial — especially new features, architecture changes, or anything in the "areas where help is needed" list. This prevents duplicated effort.
- Fork the repository and create a branch from
master:git checkout -b feat/your-feature-name
- Follow Conventional Commits (
feat,fix,perf,test,docs, etc.) with the relevant scope (smtp,queue,compliance,config,api,ui). - Ensure the PR checklist passes before requesting review:
make build— compiles without errorsmake test— all tests pass (race detector enabled)make lint— no new linter findings- New code has tests
- No breaking changes (or clearly documented)
Areas where contributions are especially useful:
- DMARC record lookup — DNS-based policy evaluation in inbound compliance checks
- SPF hardening — proper nested
include,redirect, and macro expansion per RFC 7208 - Test coverage —
internal/smtp,internal/queue,internal/compliance, andinternal/confighave gaps - Admin UI modernization — migrate the static HTML dashboard to React or Vue
- Alpine / Arch packaging —
APKBUILDandPKGBUILDto complement the existing RPM and DEB - Documentation — architecture diagrams, platform deployment guides, expanded API reference
See CONTRIBUTING.md for the full workflow and CODE_OF_CONDUCT.md for community standards.
This project is licensed under the MIT License. See LICENSE for details.