Architecture Overview
This document explains the architectural benefits of using genro-mail-proxy as an email proxy instead of directly connecting to SMTP servers from your application.
Why Use an Email Proxy?
When building enterprise applications, sending emails directly from the application to SMTP servers introduces several challenges:
Tight coupling between business logic and mail delivery
Synchronous operations that block request handlers
No built-in retry mechanisms for transient failures
Rate limiting must be implemented in every service
Connection management overhead on each send
No centralized monitoring of email delivery
Difficult debugging of delivery issues
genro-mail-proxy solves these problems by introducing a decoupled, asynchronous email delivery layer that sits between your application and SMTP servers.
Architecture Pattern
Traditional Direct SMTP
┌─────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ HTTP Request Handler │
│ ↓ │
│ 1. Process business logic │
│ 2. Open SMTP connection (500ms) ⏱ │
│ 3. Authenticate │
│ 4. Send email │
│ 5. Handle errors/retries │
│ 6. Close connection │
│ ↓ │
│ HTTP Response (1-2 seconds later) ❌ │
└──────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────┐
│ SMTP Server │
│ (Gmail/SES) │
└─────────────────┘
Problems:
❌ Request handler blocked for 1-2 seconds
❌ User waits for email to be sent
❌ SMTP errors crash the request
❌ No retry on transient failures
❌ Connection overhead on every send
❌ Rate limiting in application code
Proxy-Based Architecture
┌─────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ HTTP Request Handler │
│ ↓ │
│ 1. Process business logic │
│ 2. INSERT into email.message (10ms) │
│ 3. db.commit() │
│ 4. POST /commands/run-now (optional, 2ms) │
│ ↓ │
│ HTTP Response (50ms later) ✅ │
└──────────────────┬──────────────────────────────────┘
│
▼ (async, decoupled)
┌─────────────────────────────────────────────────────┐
│ genro-mail-proxy │
│ │
│ ┌────────────┐ ┌──────────────┐ │
│ │ Messages │───→│ SMTP Pool │───→ Send │
│ │ Queue │ │ (reuse) │ │
│ └────────────┘ └──────────────┘ │
│ │
│ - Rate limiting │
│ - Retry logic │
│ - Connection pooling │
│ - Monitoring │
│ - Delivery reports │
└──────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────┐
│ SMTP Server │
│ (Gmail/SES) │
└─────────────────┘
Benefits:
✅ Request handler returns in ~50ms
✅ User doesn’t wait for email
✅ SMTP errors don’t affect request
✅ Automatic retry on failures
✅ Connection pooling (10-50x faster)
✅ Centralized rate limiting
Key Benefits
1. Decoupling (Write vs Send Concern)
Separation of Responsibilities:
# Your Application
def create_order(order_data):
# 1. Business logic
order = db.table('orders').insert(order_data)
# 2. Email persistence (ALWAYS committed)
email = db.table('email.message').insert({
'from': 'sales@company.com',
'to': order['customer_email'],
'subject': f'Order #{order["id"]} Confirmation',
'body': render_template('order_confirmation.html', order)
})
db.commit() # ✅ Guaranteed persistence
# 3. Trigger async send (best effort)
try:
httpx.post("http://localhost:8000/commands/run-now", timeout=2)
except:
pass # Non-blocking, polling will handle it
return order
What You Get:
✅ Email record always saved - audit trail guaranteed
✅ Request completes fast - no SMTP blocking
✅ Delivery decoupled - SMTP issues don’t affect business logic
✅ Retry capability - can resend failed emails from DB
Traditional Approach Problems:
# ❌ Problematic direct SMTP
def create_order(order_data):
order = db.table('orders').insert(order_data)
try:
# ❌ Blocks request for 500-2000ms
smtp = smtplib.SMTP('smtp.gmail.com', 587)
smtp.login(user, password)
smtp.send_message(email)
smtp.quit()
db.commit() # Only commits if SMTP succeeds
except smtplib.SMTPException as e:
# ❌ Business transaction rolls back due to email error
db.rollback()
raise HTTPError(500, "Email failed")
return order
2. Resilience and Reliability
Failure Scenarios Handled:
Scenario |
Proxy Behavior |
|---|---|
SMTP server temporarily down |
Retries every 1-5 min until OK |
Network timeout |
Queues message, retries later |
Rate limit exceeded |
Defers message automatically |
Authentication failure |
Marks error, alerts operator |
Invalid recipient |
Marks error, preserves record |
Proxy service down |
Messages safe in DB, sent later |
Example: SMTP Server Maintenance
T=0:00 → User creates order
Email saved to DB ✅
Commit successful ✅
User sees "Order created" ✅
T=0:01 → Proxy tries to send
SMTP connection refused (maintenance)
Message marked for retry
T=1:00 → Proxy retries (polling)
Still down, retry again
T=5:00 → Proxy retries
Still down, retry again
T=30:00 → SMTP server back online
Proxy retries
Email sent successfully ✅
Customer receives email
With Direct SMTP: User would have seen “Order creation failed” at T=0:01 ❌
3. Performance Optimization
Connection Pooling
The proxy maintains persistent SMTP connections (5 min TTL):
┌─────────────────────────────────────────────────────┐
│ Message Batch Performance │
├─────────────────┬───────────────────┬───────────────┤
│ │ Direct SMTP │ With Proxy │
├─────────────────┼───────────────────┼───────────────┤
│ Message 1 │ 500ms (connect) │ 500ms (init) │
│ Message 2 │ 500ms (reconnect) │ 50ms (reuse) │
│ Message 3 │ 500ms (reconnect) │ 50ms (reuse) │
│ Message 4 │ 500ms (reconnect) │ 50ms (reuse) │
│ Message 5 │ 500ms (reconnect) │ 50ms (reuse) │
├─────────────────┼───────────────────┼───────────────┤
│ **Total** │ **2.5 seconds** │ **0.7 seconds**│
│ **Improvement** │ │ **3.5x faster**│
└─────────────────┴───────────────────┴───────────────┘
For high-volume scenarios (100 messages):
Direct SMTP: ~50 seconds (100 × 500ms)
With Proxy: ~5 seconds (1 × 500ms + 99 × 50ms)
Improvement: 10x faster ⚡
Async Processing
Request Latency Comparison
Direct SMTP:
├─ Business logic: 20ms
├─ SMTP connect: 300ms
├─ SMTP auth: 200ms
├─ Send email: 100ms
└─ Total: 620ms ❌
With Proxy:
├─ Business logic: 20ms
├─ DB insert: 5ms
├─ Commit: 5ms
├─ Trigger run-now: 2ms
└─ Total: 32ms ✅ (19x faster)
User Experience:
Direct SMTP: “Processing…” spinner for 1-2 seconds
With Proxy: Instant response, email sent in background
4. Centralized Rate Limiting
Problem with Direct SMTP:
# ❌ Rate limiting in every service/instance
class EmailService:
def __init__(self):
self.rate_limiter = RateLimiter(
limit_per_minute=10,
limit_per_hour=500
)
def send(self, email):
if not self.rate_limiter.check():
raise RateLimitError()
# Send email...
Issues:
❌ Each service instance has separate limiter (no coordination)
❌ Scaling to 10 servers → 10x rate limit (unintended)
❌ Manual implementation in every codebase
❌ No automatic deferral/retry
With Proxy:
# ✅ Configure once, works everywhere
POST /account
{
"id": "smtp-main",
"host": "smtp.gmail.com",
"limit_per_minute": 10,
"limit_per_hour": 500,
"limit_behavior": "defer" // or "error"
}
Benefits:
✅ Single source of truth for rate limits
✅ Shared across all application instances
✅ Automatic deferral when limit reached
✅ Respects SMTP server policies
Deferred Message Example:
T=0:00 → Message 1-10 sent (10/min limit)
T=0:05 → Message 11 arrives
Rate limit check: 10 sent in last minute
Action: Defer until T=1:00
Status: {"status": "deferred", "deferred_until": 1735689660}
T=1:00 → Automatic retry
Rate limit OK: 0 sent in last minute
Message 11 sent successfully ✅
5. Monitoring and Observability
Centralized Metrics:
The proxy exposes Prometheus metrics at GET /metrics:
# HELP gmp_sent_total Total emails sent
gmp_sent_total{account_id="smtp-main"} 1523
# HELP gmp_errors_total Total emails failed
gmp_errors_total{account_id="smtp-main"} 12
# HELP gmp_deferred_total Total emails deferred
gmp_deferred_total{account_id="smtp-main"} 45
# HELP gmp_rate_limited_total Rate limit hits
gmp_rate_limited_total{account_id="smtp-main"} 45
# HELP gmp_pending_messages Current queue size
gmp_pending_messages 3
Grafana Dashboard Example:
┌──────────────────────────────────────────────┐
│ Email Delivery Dashboard │
├──────────────────────────────────────────────┤
│ 📊 Throughput │
│ ▓▓▓▓▓▓▓▓░░ 145 emails/hour │
│ │
│ ✅ Success Rate │
│ ████████░░ 98.7% │
│ │
│ ⚠️ Error Rate │
│ ▓░░░░░░░░░ 1.3% (2 failures) │
│ │
│ 📈 Queue Size │
│ ▓▓░░░░░░░░ 3 pending │
│ │
│ ⏱️ Avg Latency │
│ 52ms (last hour) │
└──────────────────────────────────────────────┘
Alerting Rules:
# Alert if error rate > 5%
- alert: HighEmailErrorRate
expr: |
rate(gmp_errors_total[5m]) /
rate(gmp_sent_total[5m]) > 0.05
# Alert if queue growing
- alert: EmailQueueBacklog
expr: gmp_pending_messages > 100
With Direct SMTP: No centralized visibility, must check logs on each server ❌
6. Multi-Tenant Support
Multiple SMTP Accounts:
# Configure accounts for different purposes
accounts = [
{
"id": "transactional",
"host": "smtp.sendgrid.com",
"limit_per_minute": 100,
"use_tls": True
},
{
"id": "marketing",
"host": "smtp.mailgun.com",
"limit_per_minute": 50,
"use_tls": True
},
{
"id": "notifications",
"host": "email-smtp.eu-central-1.amazonaws.com",
"limit_per_minute": 10,
"use_tls": True
}
]
Route by Purpose:
# Transactional emails (high priority)
order_email = {
"account_id": "transactional",
"priority": 0, # immediate
"from": "orders@company.com",
"to": customer_email,
"subject": "Order Confirmation"
}
# Marketing emails (lower priority)
newsletter = {
"account_id": "marketing",
"priority": 3, # low
"from": "newsletter@company.com",
"to": subscriber_email,
"subject": "Monthly Newsletter"
}
# System notifications
alert = {
"account_id": "notifications",
"priority": 0, # immediate
"from": "alerts@company.com",
"to": admin_email,
"subject": "System Alert"
}
Benefits:
✅ Independent rate limits per account
✅ Different SMTP providers for different purposes
✅ Isolated failure domains
✅ Cost optimization (cheap provider for bulk, reliable for transactional)
7. Debugging and Troubleshooting
Diagnostic Tools Included:
# Check system state
python3 diagnose.py
# Output:
# 📊 Messaggi pending: 3
# 📊 Messaggi inviati: 1523
# 📊 Messaggi con errore: 2
# 🔐 Account configurati: 3
# Monitor real-time activity
python3 check_loop.py
# Output:
# ✅ Loop sta processando messaggi
# Test specific message
python3 test_dispatch.py
# Output:
# 🎉 MESSAGGIO INVIATO CON SUCCESSO!
Detailed Logs:
# With delivery_activity=true
[INFO] Attempting delivery for message msg-001 to user@example.com
[INFO] Delivery succeeded for message msg-001 (account=smtp-main)
# Error case
[WARNING] Delivery failed for message msg-002: Authentication failed
Database Inspection:
-- Find stuck messages
SELECT id, subject, error, created_at
FROM messages
WHERE sent_ts IS NULL
AND error_ts IS NOT NULL;
-- Check rate limiting
SELECT account_id, COUNT(*) as sends_last_hour
FROM send_log
WHERE timestamp > UNIX_TIMESTAMP() - 3600
GROUP BY account_id;
With Direct SMTP: Must check application logs, no centralized view ❌
8. Attachment Handling
Flexible Attachment Sources:
message = {
"attachments": [
# HTTP endpoint (proxy fetches via POST)
{
"filename": "invoice.pdf",
"storage_path": "doc_id=123",
"fetch_mode": "endpoint"
},
# External URL (proxy fetches)
{
"filename": "report.pdf",
"storage_path": "https://storage.company.com/reports/monthly.pdf",
"fetch_mode": "http_url"
},
# Base64 inline
{
"filename": "logo.png",
"storage_path": "iVBORw0KGgoAAAANSUhEUgAA...",
"fetch_mode": "base64"
},
# Local filesystem
{
"filename": "contract.pdf",
"storage_path": "/var/attachments/contracts/2025/contract.pdf",
"fetch_mode": "filesystem"
}
]
}
Benefits:
✅ Proxy handles URL fetching with timeout/retry
✅ Unified interface for different sources
✅ Memory efficient (streaming)
✅ Caching with MD5 markers
9. Priority Queuing
Message Prioritization:
# Immediate (priority=0) - sent ASAP
password_reset = {
"priority": 0, # or "immediate"
"subject": "Password Reset",
# ... processed within seconds
}
# High (priority=1) - important transactional
order_confirmation = {
"priority": 1, # or "high"
"subject": "Order Confirmation",
# ... processed within minute
}
# Medium (priority=2) - default
notification = {
"priority": 2, # or "medium"
"subject": "New Comment",
# ... processed normally
}
# Low (priority=3) - bulk/marketing
newsletter = {
"priority": 3, # or "low"
"subject": "Monthly Newsletter",
# ... processed when idle
}
Queue Processing Order:
-- Internal query (priority first, then FIFO)
SELECT * FROM messages
WHERE sent_ts IS NULL
ORDER BY priority ASC, -- 0, 1, 2, 3
created_at ASC -- oldest first
10. Scheduled Sending
Defer Messages to Future:
# Send tomorrow morning
import time
tomorrow_9am = int(time.mktime(
datetime(2025, 10, 24, 9, 0).timetuple()
))
reminder = {
"subject": "Appointment Reminder",
"body": "Your appointment is today at 10 AM",
"deferred_ts": tomorrow_9am # Unix timestamp
}
Use Cases:
✅ Appointment reminders (send 1 hour before)
✅ Scheduled newsletters (send at optimal time)
✅ Follow-up emails (send 3 days after signup)
✅ Trial expiration warnings (send 7 days before)
Comparison Summary
Aspect |
Direct SMTP |
genro-mail-proxy |
|---|---|---|
Request Latency |
500-2000ms ❌ |
20-50ms ✅ |
Resilience |
Fails on SMTP error ❌ |
Retries automatically ✅ |
Rate Limiting |
Manual per service ❌ |
Centralized, automatic ✅ |
Connection Reuse |
No (reconnect each time) ❌ |
Yes (pooled, 10-50x faster) ✅ |
Monitoring |
Application logs ❌ |
Prometheus metrics ✅ |
Debugging |
Scattered logs ❌ |
Diagnostic tools ✅ |
Decoupling |
Tight coupling ❌ |
Fully decoupled ✅ |
Priority Queuing |
No ❌ |
Yes (4 levels) ✅ |
Multi-Account |
Manual switching ❌ |
Built-in routing ✅ |
Scheduled Send |
Manual cron jobs ❌ |
Native support ✅ |
Delivery Reports |
No tracking ❌ |
Automatic reporting ✅ |
Attachment Handling |
Manual download ❌ |
URL/base64/filesystem ✅ |
When to Use This Architecture
Ideal For:
✅ Enterprise applications with high reliability requirements ✅ Multi-tenant systems with different email providers ✅ High-volume senders (>100 emails/day) ✅ Transactional emails where user experience matters ✅ Systems requiring audit trails and delivery reports ✅ Microservices architectures needing centralized email
Not Necessary For:
⚠️ Single-script tools sending 1-2 emails ⚠️ Development/testing with mock SMTP ⚠️ Ultra-low latency requirements (<10ms end-to-end)
Migration Path
Step 1: Deploy Proxy (No Code Changes)
# Deploy genro-mail-proxy
docker run -p 8000:8000 \
-v /data:/data \
-e API_TOKEN=secret \
genro-mail-proxy
Step 2: Add SMTP Account
curl -X POST http://localhost:8000/account \
-H "X-API-Token: secret" \
-d '{
"id": "smtp-main",
"host": "smtp.gmail.com",
"port": 587,
"user": "user@gmail.com",
"password": "app-password"
}'
Step 3: Update Application Code
# Before (direct SMTP)
def send_email(from_addr, to_addr, subject, body):
smtp = smtplib.SMTP('smtp.gmail.com', 587)
smtp.login(user, password)
smtp.send_message(...)
smtp.quit()
# After (via proxy)
def send_email(from_addr, to_addr, subject, body):
# 1. Persist
email_id = db.table('email.message').insert({
'from_address': from_addr,
'to_address': to_addr,
'subject': subject,
'body': body
})
db.commit()
# 2. Trigger (optional)
try:
httpx.post("http://localhost:8000/commands/run-now")
except:
pass # Polling handles it
Step 4: Monitor and Tune
# Check metrics
curl http://localhost:8000/metrics
# Adjust rate limits if needed
curl -X POST http://localhost:8000/account \
-d '{"id": "smtp-main", "limit_per_minute": 50}'
Conclusion
genro-mail-proxy provides a production-ready email delivery layer that solves common problems in enterprise email sending:
⚡ Performance - 10-50x faster via connection pooling
🔄 Resilience - Automatic retries, never loses messages
🎯 Decoupling - Business logic separated from delivery
📊 Observability - Centralized metrics and monitoring
🛡️ Rate Limiting - Automatic, shared across instances
🎛️ Control - Priority queuing, scheduled sending, multi-account
By introducing an email proxy layer, you gain enterprise-grade reliability without the complexity of implementing these features in every service.
Next Steps:
See Installation for deployment guide
See Usage for API reference
See Protocols and APIs for integration details
See
TROUBLESHOOTING.mdfor debugging guide