Closes #358
Plunk SMTP Relay Server
A production-ready SMTP relay server that accepts emails via SMTP protocol and forwards them to the Plunk API's
/v1/send endpoint.
Features
- Secure Authentication: API key-based authentication using project secrets
- Domain Verification: Validates sender domains are verified before accepting emails
- TLS Support: Supports both implicit TLS (port 465) and STARTTLS (port 587)
- Flexible Certificate Handling: Works with Traefik's acme.json or standard PEM files
- Email Parsing: Full email parsing with support for HTML and plain text
- Rate Limiting: Configurable recipient limits per email
- Production Ready: Built with TypeScript, error handling, and logging
Architecture
SMTP Client → SMTP Server → Email Parser → API /v1/send → AWS SES
The SMTP server acts as a relay:
- Accepts SMTP connections with authentication
- Validates sender domains against the database
- Parses incoming emails
- Forwards to the Plunk API
- Returns success/error to the SMTP client
Environment Variables
| Variable | Default | Description |
|---|---|---|
API_URI |
http://localhost:3000 |
Plunk API base URL |
SMTP_DOMAIN |
(empty) | SMTP domain - required when using Traefik acme.json with multiple certificates |
PORT_SECURE |
465 |
SMTPS port (implicit TLS) |
PORT_SUBMISSION |
587 |
SMTP submission port (STARTTLS) |
MAX_RECIPIENTS |
5 |
Maximum recipients per email |
CERT_PATH |
/certs |
Path to certificate files |
ACME_JSON_PATH |
/certs/acme.json |
Path to Traefik acme.json file |
See .env.self-host.example in the repository root for full configuration options.
Development
Prerequisites
- Node.js 20+
- PostgreSQL database (running via
yarn services:up) - Redis (running via
yarn services:up)
Running Locally
# Install dependencies
yarn install
# Start development server (with hot reload)
yarn workspace smtp dev
The SMTP server will start on ports 465 and 587. For local testing without TLS certificates, it will run in plaintext mode on port 587.
Testing with Telnet
# Connect to SMTP server
telnet localhost 587
# Example SMTP session:
EHLO localhost
AUTH LOGIN
cGx1bms= # base64("plunk")
<your-api-key> # your project secret (plain text)
MAIL FROM:<sender@yourdomain.com>
RCPT TO:<recipient@example.com>
DATA
Subject: Test Email
From: sender@yourdomain.com
To: recipient@example.com
This is a test email.
.
QUIT
Testing with Mail Clients
Configure your email client with:
- SMTP Server:
localhost(or your domain in production) - Port: 587 (STARTTLS) or 465 (SSL/TLS)
- Username:
plunk - Password: Your Plunk API key (project secret)
- From Address: Must use a verified domain in your Plunk project
Production Deployment
TLS Certificate Support
The SMTP server supports TLS certificates through two methods:
-
Traefik acme.json (recommended for Traefik/Dokploy users)
- Mount your Traefik acme.json file to
/certs/acme.json - Set
SMTP_DOMAINenvironment variable to select the correct certificate - The server will automatically use the certificate for your domain
- Mount your Traefik acme.json file to
-
PEM Files (standard certificate files)
- Mount
privkey.pemandfullchain.pemto/certs/ - These are standard Let's Encrypt/Certbot filenames
SMTP_DOMAINis optional when using PEM files
- Mount
If no certificates are mounted, the server will run without TLS (not recommended for production).
Docker Deployment
The SMTP server is included in the main Plunk Docker image:
Option 1: With Traefik acme.json
docker run -d \
-p 465:465 \
-p 587:587 \
-e SERVICE=all \
-e API_URI=https://api.yourdomain.com \
-e SMTP_DOMAIN=smtp.yourdomain.com \
-e DATABASE_URL=postgresql://... \
-e REDIS_URL=redis://... \
-v /path/to/acme.json:/certs/acme.json:ro \
plunk:latest
Option 2: With PEM files
docker run -d \
-p 465:465 \
-p 587:587 \
-e SERVICE=all \
-e API_URI=https://api.yourdomain.com \
-e DATABASE_URL=postgresql://... \
-e REDIS_URL=redis://... \
-v /etc/letsencrypt/live/smtp.yourdomain.com/privkey.pem:/certs/privkey.pem:ro \
-v /etc/letsencrypt/live/smtp.yourdomain.com/fullchain.pem:/certs/fullchain.pem:ro \
plunk:latest
Option 3: Without TLS (Development Only)
docker run -d \
-p 587:587 \
-e SERVICE=all \
-e API_URI=http://api.yourdomain.com \
-e DATABASE_URL=postgresql://... \
-e REDIS_URL=redis://... \
plunk:latest
⚠️ Running without TLS is not recommended for production!
DNS Configuration
Add an A record pointing to your server:
smtp.example.com. A 1.2.3.4
Optionally, add MX records if receiving email:
example.com. MX 10 smtp.example.com.
Security
Authentication
- Username must be
plunk(case-insensitive) - Password is the project secret (API key)
- Invalid credentials are rejected with proper SMTP error codes
Domain Validation
- Sender domain must be added to the project
- Sender domain must be verified (DNS records validated)
- Unverified domains are rejected with clear error messages
Rate Limiting
- Maximum 5 recipients per email by default (configurable)
- 10MB maximum message size
- Proper error handling for oversized messages
Monitoring
The SMTP server logs all operations using signale:
✅ SMTP server listening on port 465 (secure=true)
✅ SMTP server listening on port 587 (secure=false)
✅ Email relayed: sender@domain.com → recipient@example.com
❌ Sender domain is not verified or not associated with your account
Use PM2 or similar process managers to monitor the service in production.
Troubleshooting
TLS Certificate Issues
If TLS is not working:
- Verify certificates are mounted correctly:
docker exec <container> ls -la /certs/ - Check certificate permissions (should be readable)
- Review logs for certificate loading errors:
docker logs <container> 2>&1 | grep -i "cert" - If using Traefik acme.json, ensure
SMTP_DOMAINis set correctly - If using PEM files, ensure both
privkey.pemandfullchain.pemare present
Connection Refused
If clients cannot connect:
- Verify ports 465 and 587 are exposed and not blocked by firewall
- Check if the service is running:
pm2 list - Review logs:
pm2 logs smtp
Authentication Failures
If authentication fails:
- Verify username is exactly
plunk - Verify password is the project secret, not public key
- Check database connectivity
Domain Verification Errors
If emails are rejected with domain errors:
- Verify domain is added to your project
- Check domain verification status in Plunk dashboard
- Ensure DNS records are properly configured
Performance Considerations
The SMTP server is designed for high-scale email sending:
- Async Processing: All database and API calls are non-blocking
- Connection Pooling: Prisma handles database connection pooling
- Minimal Memory: Streams email data without buffering entire messages
- Fast Authentication: Single database query per connection
For high-volume sending:
- Deploy multiple SMTP server instances behind a load balancer
- Use connection pooling in your SMTP clients
- Monitor API rate limits and adjust accordingly
API Integration
The SMTP server forwards emails to the API endpoint:
POST /v1/send
Authorization: Bearer {project_secret}
{
"from": "sender@domain.com",
"name": "Sender Name",
"to": ["recipient@example.com"],
"subject": "Email Subject",
"body": "<html>...</html>"
}
The API response is translated to SMTP status codes:
200 OK→250 Message accepted4xx/5xx→554 Transaction failed
Contributing
When modifying the SMTP server:
- Follow the existing code patterns
- Add proper error handling
- Update TypeScript types
- Test with real SMTP clients
- Update this README if adding features
License
Part of the Plunk project - see repository root for license information.