Self-hosted email delivery infrastructure
Fast. Reliable. Yours.
Your App → Sendry → Recipient
(SMTP/API)
A complete email sending solution you can run on your own servers:
- MTA Server — SMTP relay with queue, retries, DKIM, TLS
- HTTP API — REST interface for sending and tracking emails
- Web Panel — Manage templates, campaigns, and recipients
- Secure — Let's Encrypt, DKIM signing, auth protection
- Observable — Prometheus metrics, detailed logging
- Scalable — Persistent queue, concurrent workers
- Flexible — Multi-domain, rate limiting, sandbox mode
- Manageable — Web UI for everything
- Teams sending transactional emails (confirmations, alerts, reports)
- Marketers running email campaigns without per-message costs
- Developers who need a local SMTP server for testing
- Organizations requiring email archiving and compliance
- SMTP server (ports 25, 587) with AUTH support
- SMTPS server (port 465) with implicit TLS
- STARTTLS support for secure connections
- Let's Encrypt (ACME) automatic certificate management
- DKIM signing for outgoing emails
- HTTP API for sending emails
- Persistent queue with BoltDB
- Retry logic with exponential backoff
- Multi-domain support with different modes:
production- normal deliverysandbox- capture emails locally (for testing)redirect- redirect all emails to specified addressesbcc- normal delivery + copy to archive
- Rate limiting (per domain, sender, IP, API key)
- Prometheus metrics with persistence
- Bounce handling
- Graceful shutdown
- Structured JSON logging
# Debian/Ubuntuecho"deb [trusted=yes] https://foxzi.github.io/sendry/apt stable main"| sudo tee /etc/apt/sources.list.d/sendry.list
sudo apt update && sudo apt install sendry
# RHEL/CentOS/Fedora
sudo tee /etc/yum.repos.d/sendry.repo << 'EOF'[sendry]name=Sendry Repositorybaseurl=https://foxzi.github.io/sendry/rpm/stable/$basearch/enabled=1gpgcheck=0EOF
sudo dnf install sendry
# Alpine
wget https://github.com/foxzi/sendry/releases/latest/download/sendry_0.4.18-r1_x86_64.apk
apk add --allow-untrusted sendry_*.apkwget https://github.com/foxzi/sendry/releases/latest/download/sendry-linux-amd64
chmod +x sendry-linux-amd64
sudo mv sendry-linux-amd64 /usr/local/bin/sendrydocker pull ghcr.io/foxzi/sendry:latest
docker run -p 25:25 -p 587:587 -p 8080:8080 \
-v /path/to/config.yaml:/etc/sendry/config.yaml \
ghcr.io/foxzi/sendry:latestFor running both Sendry MTA and Web management panel, see Docker documentation.
git clone https://github.com/foxzi/sendry.git
cd sendry
cp configs/sendry.example.yaml configs/sendry.yaml
cp configs/web.example.yaml configs/web.yaml
# Edit configs
docker compose up -dFor automated deployment on multiple servers, see Ansible documentation.
cd ansible
cp inventory/hosts.yml.example inventory/hosts.yml
# Edit hosts.yml with your servers
ansible-playbook -i inventory/hosts.yml playbooks/sendry.ymlRequires Go 1.24+
git clone https://github.com/foxzi/sendry.git
cd sendry
make buildFor detailed quick start guide see docs/quickstart.md.
Copy and edit the example configuration:
cp configs/sendry.example.yaml config.yamlMinimal configuration:
server:
hostname: "mail.example.com"smtp:
domain: "example.com"auth:
required: trueusers:
myuser: "mypassword"api:
api_key: "your-secret-api-key"storage:
path: "./data/queue.db"./sendry serve -c config.yaml./sendry config validate -c config.yamlcurl http://localhost:8080/healthResponse:
{
"status": "ok",
"version": "0.4.18",
"uptime": "1h30m",
"queue": {
"pending": 5,
"sending": 1,
"delivered": 100,
"failed": 2,
"deferred": 3,
"total": 111
}
}curl -X POST http://localhost:8080/api/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "from": "sender@example.com", "to": ["recipient@example.com"], "subject": "Test", "body": "Hello, World!", "html": "<p>Hello, <b>World</b>!</p>" }'Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending"
}curl http://localhost:8080/api/v1/status/{message_id} \
-H "Authorization: Bearer YOUR_API_KEY"Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "delivered",
"from": "sender@example.com",
"to": ["recipient@example.com"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:05Z",
"retry_count": 0
}curl http://localhost:8080/api/v1/queue \
-H "Authorization: Bearer YOUR_API_KEY"curl -X DELETE http://localhost:8080/api/v1/queue/{message_id} \
-H "Authorization: Bearer YOUR_API_KEY"| Parameter | Default | Description |
|---|---|---|
server.hostname | OS hostname | Server FQDN |
smtp.listen_addr | :25 | SMTP relay port |
smtp.submission_addr | :587 | SMTP submission port |
smtp.smtps_addr | :465 | SMTPS port (implicit TLS) |
smtp.domain | required | Mail domain |
smtp.max_message_bytes | 10485760 | Max message size (10MB) |
smtp.max_recipients | 100 | Max recipients per message |
smtp.auth.required | false | Require authentication |
smtp.auth.users | {} | Username -> password map |
smtp.auth.max_failures | 5 | Max auth failures before blocking |
smtp.auth.block_duration | 15m | How long to block after max failures |
smtp.auth.failure_window | 5m | Window for counting failures |
smtp.tls.cert_file | "" | TLS certificate file path |
smtp.tls.key_file | "" | TLS private key file path |
smtp.tls.acme.enabled | false | Enable Let's Encrypt |
smtp.tls.acme.email | "" | ACME account email |
smtp.tls.acme.domains | [] | Domains for certificate |
smtp.tls.acme.cache_dir | /var/lib/sendry/certs | Certificate cache |
dkim.enabled | false | Enable DKIM signing |
dkim.selector | "" | DKIM selector |
dkim.domain | "" | DKIM domain |
dkim.key_file | "" | DKIM private key path |
api.listen_addr | :8080 | HTTP API port |
api.api_key | "" | API key (empty = no auth) |
api.max_header_bytes | 1048576 | Max HTTP header size (1MB) |
api.read_timeout | 30s | HTTP read timeout |
api.write_timeout | 30s | HTTP write timeout |
api.idle_timeout | 60s | HTTP idle timeout |
queue.workers | 4 | Number of delivery workers |
queue.retry_interval | 5m | Base retry interval |
queue.max_retries | 5 | Max delivery attempts |
storage.path | /var/lib/sendry/queue.db | BoltDB file path |
storage.retention.delivered_max_age | 0 | Delete delivered messages older than this |
storage.retention.cleanup_interval | 1h | Cleanup interval |
dlq.enabled | true | Enable dead letter queue |
dlq.max_age | 0 | Delete DLQ messages older than this |
dlq.max_count | 0 | Max DLQ messages (0 = unlimited) |
dlq.cleanup_interval | 1h | DLQ cleanup interval |
logging.level | info | Log level (debug/info/warn/error) |
logging.format | json | Log format (json/text) |
metrics.enabled | false | Enable Prometheus metrics |
metrics.listen_addr | :9090 | Metrics server port |
metrics.path | /metrics | Metrics endpoint path |
metrics.flush_interval | 10s | Counter persistence interval |
metrics.allowed_ips | [] | IPs/CIDRs allowed to access metrics |
See documentation:
- HTTP API reference
- TLS and DKIM
- Message retention and DLQ
- Rate limiting
- Prometheus metrics
- Sendry Web panel
- DNS sync (Cloudflare)
- Ansible deployment
sendry/
├── cmd/sendry/ # CLI entry point
├── internal/
│ ├── api/ # HTTP API server
│ ├── app/ # Application orchestration
│ ├── config/ # Configuration
│ ├── dkim/ # DKIM signing
│ ├── dns/ # MX resolver
│ ├── metrics/ # Prometheus metrics
│ ├── queue/ # Message queue & storage
│ ├── smtp/ # SMTP server & client
│ └── tls/ # TLS/ACME support
├── configs/ # Example configurations
└── docs/ # Documentation
GPL-3.0