Postfix Setup Guide for Postchi
Architecture Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ API │─────▶│ Redis Queue │─────▶│ Worker │─────▶│ Postfix │
│ (Node.js) │ │ (BullMQ) │ │ (Node.js) │ │ (SMTP) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ Internet
│ PostgreSQL │ │ DKIM Signing│
│ (Domains, │ │ (Nodemailer)│
│ DKIM Keys)│ └─────────────┘
└─────────────┘
Flow:
- User sends email via API → Domain validation
- API creates
Messagerecord in DB with DKIM keys fromDomaintable - Email queued in Redis (BullMQ)
- Worker picks up job, signs email with DKIM (Nodemailer handles signing)
- Worker sends to Postfix via SMTP (authenticated)
- Postfix relays to recipient's mail server
Key Points:
- DKIM signing happens in Nodemailer (worker), NOT in Postfix
- Postfix acts as a relay server, not a signer
- Each organization's domains can have their own DKIM keys
1. Postfix Server Configuration
Installation
sudo apt update
sudo apt install postfix postfix-pcre sasl2-bin libsasl2-modules
Select "Internet Site" during installation.
Main Configuration (/etc/postfix/main.cf)
# ============================================
# BASIC IDENTIFICATION
# ============================================
myhostname = mail.postchi.io
mydomain = postchi.io
myorigin = $mydomain
# Network
inet_interfaces = all
inet_protocols = ipv4
# ============================================
# RELAY CONFIGURATION (IMPORTANT)
# ============================================
# Only accept mail from authenticated clients
mynetworks = 127.0.0.0/8, [::1]/128
relay_domains =
smtpd_relay_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
# Accept mail from anywhere (for receiving bounces)
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_invalid_hostname,
reject_non_fqdn_hostname,
reject_non_fqdn_sender,
reject_non_fqdn_recipient,
reject_unknown_sender_domain,
reject_unknown_recipient_domain,
reject_rbl_client zen.spamhaus.org,
permit
# ============================================
# AUTHENTICATION (SASL)
# ============================================
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
broken_sasl_auth_clients = yes
# ============================================
# TLS/SSL CONFIGURATION
# ============================================
# For development - use self-signed
smtpd_tls_cert_file = /etc/ssl/certs/postfix-selfsigned.crt
smtpd_tls_key_file = /etc/ssl/private/postfix-selfsigned.key
# For production - use Let's Encrypt
# smtpd_tls_cert_file = /etc/letsencrypt/live/mail.postchi.io/fullchain.pem
# smtpd_tls_key_file = /etc/letsencrypt/live/mail.postchi.io/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes
smtpd_tls_session_cache_timeout = 3600s
# SMTP client TLS (outbound)
smtp_tls_security_level = may
smtp_tls_loglevel = 1
# ============================================
# MESSAGE SIZE & LIMITS
# ============================================
message_size_limit = 26214400 # 25MB (matches your worker config)
mailbox_size_limit = 0
# ============================================
# RATE LIMITING (ANTI-ABUSE)
# ============================================
smtpd_client_connection_rate_limit = 100
smtpd_client_message_rate_limit = 100
smtpd_client_recipient_rate_limit = 200
# Per-recipient limits
anvil_rate_time_unit = 60s
smtpd_client_event_limit_exceptions = $mynetworks
# ============================================
# LOGGING
# ============================================
maillog_file = /var/log/postfix.log
# ============================================
# BOUNCE HANDLING
# ============================================
# Send bounces back to your system for processing
notify_classes = bounce, 2bounce, delay, policy, protocol, resource, software
bounce_notice_recipient = bounces@postchi.io
Master Configuration (/etc/postfix/master.cf)
# ============================================
# SMTP SERVICE (Port 25) - For receiving bounces
# ============================================
smtp inet n - y - - smtpd
# ============================================
# SUBMISSION SERVICE (Port 587) - For your worker
# ============================================
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_auth_only=yes
-o smtpd_reject_unlisted_recipient=no
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
# ============================================
# SMTPS SERVICE (Port 465) - Optional (legacy)
# ============================================
# smtps inet n - y - - smtpd
# -o syslog_name=postfix/smtps
# -o smtpd_tls_wrappermode=yes
# -o smtpd_sasl_auth_enable=yes
# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
2. Authentication Setup (SASL)
Option A: Dovecot SASL (Recommended for Production)
Install Dovecot:
sudo apt install dovecot-core
Configure /etc/dovecot/conf.d/10-master.conf:
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
Create user database /etc/dovecot/users:
postchi_worker:{PLAIN}your_secure_password
Set permissions:
sudo chmod 640 /etc/dovecot/users
sudo chown root:dovecot /etc/dovecot/users
Option B: Cyrus SASL (Simpler for Testing)
Create /etc/postfix/sasl/smtpd.conf:
pwcheck_method: auxprop
auxprop_plugin: sasldb
mech_list: PLAIN LOGIN
Add user:
sudo saslpasswd2 -c -u postchi.io postchi_worker
# Enter password when prompted
Verify:
sudo sasldblistusers2
Option C: No Auth (Local Testing Only)
If worker runs on same machine:
# In main.cf
mynetworks = 127.0.0.0/8
smtpd_relay_restrictions = permit_mynetworks, reject
3. TLS/SSL Certificate Setup
Development (Self-Signed)
sudo openssl req -new -x509 -days 365 -nodes \
-out /etc/ssl/certs/postfix-selfsigned.crt \
-keyout /etc/ssl/private/postfix-selfsigned.key
sudo chmod 600 /etc/ssl/private/postfix-selfsigned.key
Production (Let's Encrypt)
sudo apt install certbot
sudo certbot certonly --standalone -d mail.postchi.io
# Update main.cf to use:
# smtpd_tls_cert_file = /etc/letsencrypt/live/mail.postchi.io/fullchain.pem
# smtpd_tls_key_file = /etc/letsencrypt/live/mail.postchi.io/privkey.pem
4. Firewall Configuration
# Allow SMTP ports
sudo ufw allow 25/tcp # SMTP (receiving)
sudo ufw allow 587/tcp # Submission (your worker)
sudo ufw allow 465/tcp # SMTPS (optional)
# Check status
sudo ufw status
5. DNS Records Configuration
For each domain your users add, they need to configure these DNS records:
Required Records
1. MX Record (for receiving bounces)
Type: MX
Host: @
Value: mail.postchi.io
Priority: 10
TTL: 3600
2. SPF Record (sender policy framework)
Type: TXT
Host: @
Value: v=spf1 ip4:YOUR_SERVER_IP include:mail.postchi.io ~all
TTL: 3600
Your code generates this at /packages/api/src/utils/dkim.ts:52-78
3. DKIM Record (generated per domain)
Type: TXT
Host: default._domainkey
Value: v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4G...
TTL: 3600
Your code generates this at /packages/api/src/utils/dkim.ts:38-47
Selector: Stored in Domain.dkimSelector (default: "default")
4. DMARC Record (email authentication policy)
Type: TXT
Host: _dmarc
Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@postchi.io
TTL: 3600
Your code generates this at /packages/api/src/utils/dkim.ts:83-121
Example DNS Configuration for user domain example.com:
; MX Record
example.com. 3600 IN MX 10 mail.postchi.io.
; SPF Record
example.com. 3600 IN TXT "v=spf1 ip4:203.0.113.1 include:mail.postchi.io ~all"
; DKIM Record
default._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
; DMARC Record
_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@postchi.io"
; Domain verification (temporary)
example.com. 3600 IN TXT "postchi-verify-abc123def456..."
6. Environment Variables Configuration
Update your .env file:
Development (Local Testing)
# SMTP Configuration - Postfix
SMTP_HOST=localhost
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
SMTP_PASS=
# For local testing without auth
# SMTP_PORT=25 # or use submission port
Production
# SMTP Configuration - Postfix
SMTP_HOST=mail.postchi.io
SMTP_PORT=587
SMTP_SECURE=true
SMTP_USER=postchi_worker
SMTP_PASS=your_secure_password_from_sasl_setup
7. Testing Plan
Step 1: Test Postfix Basics
# Check Postfix status
sudo systemctl status postfix
# View logs
sudo tail -f /var/log/postfix.log
# Test SMTP connection
telnet localhost 25
Step 2: Test SASL Authentication
# Install test tool
sudo apt install swaks
# Test authenticated submission
swaks --to test@example.com \
--from noreply@postchi.io \
--server localhost:587 \
--auth LOGIN \
--auth-user postchi_worker \
--auth-password your_password \
--tls
Step 3: Test from Worker
Update your worker .env:
SMTP_HOST=localhost # or your server IP
SMTP_PORT=587
SMTP_USER=postchi_worker
SMTP_PASS=your_password
Send test email via API:
curl -X POST http://localhost:3000/api/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": {"email": "test@yourdomain.com"},
"to": [{"email": "recipient@example.com"}],
"subject": "Test Email",
"html": "<p>This is a test email</p>"
}'
Check logs:
# Worker logs
npm run worker # or check Docker logs
# Postfix logs
sudo tail -f /var/log/postfix.log
Step 4: Verify DKIM Signing
Send email to a Gmail address, then:
- Open the email in Gmail
- Click "Show original"
- Check for:
DKIM: 'PASS'SPF: 'PASS'DMARC: 'PASS'
Or use online tools:
Step 5: Test Bounce Handling
Send to invalid address:
curl -X POST http://localhost:3000/api/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"from": {"email": "test@yourdomain.com"},
"to": [{"email": "invalid@nonexistent-domain-12345.com"}],
"subject": "Bounce Test",
"text": "This should bounce"
}'
Check:
- Postfix logs for bounce message
- Database
MessageEventtable forBOUNCEDevent
8. DKIM Key Management Workflow
Your application already has the utilities, here's how it works:
When User Adds Domain (POST /api/domains)
// In domain.service.ts
import { generateDkimKeys, formatDkimPublicKeyForDns, generateSpfRecord, generateDmarcRecord, generateVerificationToken } from '../utils/dkim.js';
// 1. Generate DKIM keys
const dkimKeys = generateDkimKeys('default');
// 2. Generate DNS records
const spfRecord = generateSpfRecord({
ip4Addresses: [process.env.SERVER_IP],
includeDomains: ['mail.postchi.io']
});
const dmarcRecord = generateDmarcRecord({
policy: 'quarantine',
rua: ['dmarc@postchi.io']
});
const dkimRecord = formatDkimPublicKeyForDns(dkimKeys.publicKey);
// 3. Store in database
await prisma.domain.create({
data: {
domain: 'example.com',
organizationId,
dkimSelector: 'default',
dkimPrivateKey: dkimKeys.privateKey, // TODO: Encrypt this!
dkimPublicKey: dkimKeys.publicKey,
spfRecord,
dmarcRecord,
dkimRecord,
verificationToken: generateVerificationToken(),
verificationStatus: 'PENDING'
}
});
Domain Verification
User adds TXT record:
Type: TXT
Host: @
Value: postchi-verify-abc123def456...
Your API checks:
// Verify DNS TXT record exists
const dns = require('dns').promises;
const records = await dns.resolveTxt(domain);
const verified = records.some(r => r.includes(verificationToken));
When Sending Email
Worker retrieves DKIM keys from database:
// email.worker.ts already does this on lines 168-170
const transporter = createSmtpTransporter({
domainName: data.domain,
keySelector: data.dkimSelector,
privateKey: data.dkimPrivateKey // Retrieved from DB
});
Nodemailer signs the email automatically.
9. Security Considerations
MUST DO:
-
Encrypt DKIM private keys in database
- Use
crypto.publicEncrypt()with a master key - Store master key in secure environment variable
- Decrypt only in worker when sending
- Use
-
Use TLS for SMTP connections (port 587)
- Set
SMTP_SECURE=truein production
- Set
-
Implement rate limiting per organization
- Already in schema:
emailsPerHour,emailsPerDay - Enforce in API before queueing
- Already in schema:
-
Firewall rules
- Only allow port 587 from your worker server IPs
- Use SSH key auth, disable password login
-
Monitor bounce rates
- High bounce = possible spam
- Auto-pause domains with >10% bounce rate
NICE TO HAVE:
- IP warm-up strategy (gradual increase in sending volume)
- Dedicated IPs for high-volume senders
- Feedback Loop (FBL) setup with major ISPs
- Regular DKIM key rotation (every 6 months)
10. Postfix Commands Cheatsheet
# Service management
sudo systemctl start postfix
sudo systemctl stop postfix
sudo systemctl restart postfix
sudo systemctl status postfix
# View mail queue
mailq
postqueue -p
# Flush queue (retry delivery)
postqueue -f
# Delete all mail in queue
sudo postsuper -d ALL
# Delete deferred mail
sudo postsuper -d ALL deferred
# View logs
sudo tail -f /var/log/postfix.log
sudo grep "ERROR" /var/log/postfix.log
# Test configuration
sudo postfix check
# Reload after config change
sudo postfix reload
# View active connections
sudo postconf -n | grep smtp
# Check SASL users (Cyrus)
sudo sasldblistusers2
# Test TLS
openssl s_client -connect localhost:587 -starttls smtp
11. Troubleshooting
Worker can't connect to Postfix
# Check Postfix is listening
sudo netstat -tlnp | grep master
# Check SASL users exist
sudo sasldblistusers2
# Test connection manually
telnet localhost 587
# Check firewall
sudo ufw status
Email sent but not received
# Check queue
mailq
# Check recipient's mail server
nslookup -type=MX recipient-domain.com
# Check SPF/DKIM/DMARC
# Send to mail-tester.com and check score
DKIM signature fails
- Check public key in DNS matches database
- Verify selector is correct (
default._domainkey) - Check private key format (PEM)
- Test with online DKIM validator
High bounce rate
- Check DNS records (SPF, DKIM, DMARC)
- Verify sending IP not blacklisted: https://mxtoolbox.com/blacklists.aspx
- Implement double opt-in for contacts
- Clean your contact lists regularly
Summary
Your Current Setup:
- ✅ DKIM signing in Nodemailer (worker)
- ✅ DKIM key generation utilities
- ✅ Database schema for domain management
- ✅ Queue-based email processing
- ❌ DKIM key encryption (TODO)
- ❌ Automated domain verification (TODO)
Postfix Role:
- Acts as authenticated SMTP relay
- Does NOT sign emails (Nodemailer does)
- Receives bounces and feedback
- Handles TLS/authentication
Next Steps:
- Set up Postfix on your server with above config
- Configure SASL authentication
- Add your worker's credentials to
.env - Test sending with your domain
- Verify DKIM/SPF/DMARC pass
- Implement DKIM key encryption before production