Example Client
The example_client.py demonstrates how to integrate your application with genro-mail-proxy.
Overview
This standalone server shows the recommended integration pattern:
Local Persistence - Write messages to your application’s database first
Async Submission - Submit messages to mail service via REST API
Immediate Trigger - Optionally trigger
run-nowfor fast deliveryDelivery Reports - Receive confirmations via
proxy_syncendpoint
This pattern ensures:
✅ Never lose messages - Committed locally before submission
✅ Decoupled architecture - Mail service downtime doesn’t block your app
✅ Fast delivery - 99% of messages sent in <2 seconds via run-now trigger
✅ Guaranteed delivery - Polling ensures missed triggers are recovered
✅ Full tracking - Local database tracks message lifecycle
Quick Start
Installation
# Install dependencies
pip install fastapi uvicorn aiohttp
# Configure recipient email
nano example_config.ini
# Edit: recipient_email = your@email.com
# Start the example client
python3 example_client.py
The server will start on http://localhost:8081.
Configuration
Edit example_config.ini:
[mail_service]
url = http://localhost:8000
api_token = your-secret-token
[test]
# CHANGE THIS to your actual email address
recipient_email = your@email.com
sender_email = test@example.com
sender_name = Example Client Test
[database]
path = example_client.db
Usage Examples
Send Single Test Email
curl -X POST http://localhost:8081/send-test-email
Response:
{
"status": "success",
"message": "Successfully queued 1 test email(s)",
"messages": ["test_1761215432000_1234"],
"mail_service_response": {
"queued": 1,
"rejected": []
},
"note": "Check /stats endpoint to monitor delivery status"
}
Send Multiple Test Emails
# Send 5 test emails
curl -X POST http://localhost:8081/send-test-email?count=5
Custom Subject Line
curl -X POST "http://localhost:8081/send-test-email?subject=Integration%20Test"
View Message Statistics
curl http://localhost:8081/stats
Response:
{
"status": "success",
"statistics": {
"total": 10,
"pending": 0,
"submitted": 2,
"delivered": 7,
"failed": 1
}
}
List Pending Messages
curl http://localhost:8081/messages
Response:
{
"status": "success",
"count": 2,
"messages": [
{
"id": "test_1761215432000_1234",
"recipient": "your@email.com",
"subject": "Test Email from Example Client",
"status": "submitted",
"created_at": "2025-10-23T10:30:32",
"submitted_at": "2025-10-23T10:30:33"
}
]
}
Architecture
Integration Flow
┌──────────────────┐
│ Your Application│
│ (example_client)│
└────────┬─────────┘
│
│ 1. Write to local DB
├─────────────────────────────┐
│ ▼
│ ┌─────────────────┐
│ │ Local Database │
│ │ (example_client│
│ │ .db) │
│ └─────────────────┘
│
│ 2. POST /commands/add-messages
│
▼
┌──────────────────┐
│ genro-mail-proxy │────────► SMTP Server
└────────┬─────────┘
│
│ 3. POST /delivery-report (to your app)
│ (delivery reports)
│
▼
┌──────────────────┐
│ Your Application│
│ (example_client)│
│ │
│ 4. Update local │
│ DB status │
└──────────────────┘
Message States
Messages transition through these states in the local database:
pending - Created locally, not yet submitted to mail service
submitted - Sent to mail service, awaiting delivery
delivered - Confirmation received from mail service
failed - Permanent error reported by mail service
Database Schema
The example client maintains this schema:
CREATE TABLE outbound_emails (
id TEXT PRIMARY KEY,
recipient TEXT NOT NULL,
subject TEXT NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL,
submitted_at INTEGER,
delivered_at INTEGER,
error TEXT,
status TEXT DEFAULT 'pending'
);
CREATE INDEX idx_status ON outbound_emails(status);
Integration Pattern Explained
Why This Pattern?
This pattern solves the common problem: What happens if the mail service is down?
❌ Direct SMTP approach:
# BAD: If SMTP server is down, message is lost
smtp.send_message(msg)
# User's email is gone forever!
❌ Naive proxy approach:
# BAD: If mail service is down, message is lost
requests.post('http://mail-service/send', json=msg)
# User's email is gone forever!
✅ Decoupled approach (this example):
# GOOD: Persist locally FIRST
db.execute("INSERT INTO outbound_emails ...")
db.commit() # Message is safe!
# THEN submit to mail service (best effort)
try:
requests.post('http://mail-service/add-messages', json=msg)
requests.post('http://mail-service/run-now') # trigger immediate send
except ConnectionError:
# No problem! Polling will pick it up later
pass
# Your polling loop ensures delivery
# Even if trigger fails, message will be sent within 5 minutes
Benefits
Local-first persistence - Message committed before network calls
Resilient to downtime - Mail service can be restarted without data loss
Non-blocking - Your application doesn’t wait for SMTP
Auditable - Full lifecycle tracked in your database
Testable - Easy to mock and test
Monitorable - Query local DB for delivery status
Performance Characteristics
Typical latencies:
Write to local DB: <5ms
Submit to mail service: 10-30ms (non-blocking HTTP POST)
Trigger run-now: 5-15ms (best-effort)
Actual SMTP delivery: 100-500ms (handled asynchronously)
From user’s perspective:
API response time: ~50ms (local DB write + HTTP POST)
Email arrives in inbox: <2 seconds (99% with run-now trigger)
Fallback via polling: <5 minutes (1% if trigger fails)
Code Walkthrough
Step 1: Create Message Locally
async def create_test_message(self):
"""Write to YOUR database first."""
message_id = f"test_{int(time.time() * 1000)}"
# Local persistence FIRST
conn = sqlite3.connect(self.db_path)
conn.execute("""
INSERT INTO outbound_emails (id, recipient, subject, body, created_at)
VALUES (?, ?, ?, ?, ?)
""", (message_id, recipient, subject, body, now))
conn.commit()
conn.close()
return {'id': message_id, 'to': recipient, ...}
Key point: Message is safe in your database before any network calls.
Step 2: Submit to Mail Service
async def submit_to_mail_service(self, messages):
"""Hand off to mail service."""
headers = {'X-API-Token': self.api_token}
url = f"{self.mail_service_url}/commands/add-messages"
async with aiohttp.ClientSession() as session:
async with session.post(url, json={'messages': messages}, headers=headers) as resp:
result = await resp.json()
# Update local tracking
conn = sqlite3.connect(self.db_path)
for msg in messages:
conn.execute("""
UPDATE outbound_emails
SET submitted_at = ?, status = 'submitted'
WHERE id = ?
""", (now, msg['id']))
conn.commit()
Key point: Non-blocking submission, local DB tracks submission timestamp.
Step 3: Trigger Immediate Dispatch
async def trigger_immediate_dispatch(self):
"""Wake up SMTP loop (best-effort)."""
try:
headers = {'X-API-Token': self.api_token}
async with session.post(f"{self.url}/commands/run-now", headers=headers):
logger.info("Triggered immediate dispatch")
except aiohttp.ClientError as e:
logger.warning(f"Trigger failed (non-fatal): {e}")
# Not a problem! Polling will handle it
Key point: This is a best-effort optimization, not required for correctness.
Step 4: Receive Delivery Reports
@app.post("/delivery-report")
async def delivery_report(request: Request):
"""Handle delivery confirmations from mail service."""
data = await request.json()
reports = data.get('reports', [])
conn = sqlite3.connect(client.db_path)
for report in reports:
if report['status'] == 'sent':
conn.execute("""
UPDATE outbound_emails
SET delivered_at = ?, status = 'delivered'
WHERE id = ?
""", (now, report['id']))
elif report['status'] == 'error':
conn.execute("""
UPDATE outbound_emails
SET error = ?, status = 'failed'
WHERE id = ?
""", (report['error'], report['id']))
conn.commit()
return {'status': 'ok', 'processed': len(reports)}
Key point: Your application knows the final delivery status.
API Reference
Endpoints
POST /send-test-email
Generate and send test email(s).
Query Parameters:
count(int, optional): Number of emails to send (1-100, default 1)subject(str, optional): Custom subject line
Response:
{
"status": "success",
"message": "Successfully queued 1 test email(s)",
"messages": ["test_1761215432000_1234"],
"mail_service_response": {
"queued": 1,
"rejected": []
}
}
GET /messages
List pending messages.
Response:
{
"status": "success",
"count": 2,
"messages": [
{
"id": "test_1761215432000_1234",
"recipient": "your@email.com",
"subject": "Test Email",
"status": "submitted",
"created_at": "2025-10-23T10:30:32",
"submitted_at": "2025-10-23T10:30:33"
}
]
}
GET /stats
Get message statistics.
Response:
{
"status": "success",
"statistics": {
"total": 10,
"pending": 0,
"submitted": 2,
"delivered": 7,
"failed": 1
}
}
POST /email/mailproxy/mp_endpoint/proxy_sync
Receive delivery reports from mail service (internal endpoint).
Request Body:
{
"reports": [
{
"id": "test_1761215432000_1234",
"status": "sent",
"timestamp": 1761215435
}
]
}
Response:
{
"status": "ok",
"processed": 1
}
Testing
End-to-End Test
Start mail service:
# In terminal 1 cd /path/to/genro-mail-proxy python3 main.py
Start example client:
# In terminal 2 python3 example_client.py
Send test email:
# In terminal 3 curl -X POST http://localhost:8081/send-test-email
Monitor delivery:
# Check example client stats curl http://localhost:8081/stats # Check mail service status curl http://localhost:8000/status
Verify email:
Check your inbox for the test email (configured in
example_config.ini).
Expected Timeline
T+0ms: POST /send-test-email
T+5ms: Message written to example_client.db (status=pending)
T+30ms: Submitted to mail service (status=submitted)
T+35ms: run-now trigger sent
T+40ms: Mail service wakes SMTP loop
T+500ms: SMTP delivery completes
T+5000ms: Delivery report posted to proxy_sync (status=delivered)
Total user-visible latency: ~35ms (steps 1-3) Total email delivery time: ~500ms (includes SMTP) Confirmation received: ~5 seconds (via proxy_sync)
Troubleshooting
Client Won’t Start
Error: ModuleNotFoundError: No module named 'fastapi'
Solution:
pip install fastapi uvicorn aiohttp
Mail Service Connection Refused
Error: Mail service unavailable: Cannot connect to host localhost:8000
Solution:
Verify mail service is running:
curl http://localhost:8000/statusCheck
example_config.iniURL is correct:[mail_service] url = http://localhost:8000 # Match mail service port
Authentication Failed
Error: Mail service error: Invalid API token
Solution:
Ensure example_config.ini token matches the mail service token:
# example_config.ini
[mail_service]
api_token = your-secret-token
# Check mail service token with CLI
mail-proxy myserver token
Messages Stuck in “submitted” State
Symptoms: curl http://localhost:8081/stats shows submitted > 0 for extended period.
Diagnosis:
Check mail service is processing:
curl http://localhost:8000/statusCheck for delivery errors:
curl http://localhost:8000/messages | jq '.messages[] | select(.error_ts != null)'
Check SMTP account configuration:
curl http://localhost:8000/accounts
Common causes:
Mail service test_mode=true (requires manual run-now)
Invalid SMTP credentials
Rate limiting (check deferred_ts)
Network connectivity to SMTP server
Solutions:
See the FAQ for common issues and solutions.
Adapting for Your Application
To integrate this pattern into your application:
Add outbound_emails table to your existing database schema
Create wrapper function around your existing email-sending code:
async def send_email(recipient, subject, body): # 1. Write to YOUR database message_id = await db.insert_email(recipient, subject, body) # 2. Submit to mail service await mail_service.add_messages([{ 'id': message_id, 'to': recipient, 'subject': subject, 'body': body }]) # 3. Trigger immediate dispatch (best-effort) await mail_service.run_now() return message_id
Add proxy_sync endpoint to receive delivery reports
Add polling mechanism (optional, for applications that don’t receive run-now triggers)
Multi-tenant Integration
For multi-tenant deployments, each tenant can have their own sync endpoint. See Multi-tenancy Architecture for the complete architecture.
The proxy sends delivery reports with this payload structure:
{
"delivery_report": [
{
"id": "msg-001",
"account_id": "smtp-tenant1",
"priority": 2,
"sent_ts": 1705750800,
"error_ts": null,
"error": null,
"deferred_ts": null
}
]
}
Your endpoint should respond with a JSON object:
{
"ok": true,
"queued": 0,
"next_sync_after": null
}
Response fields:
ok(bool, required):trueif reports processed successfullyqueued(int, optional): Pending messages count. When > 0, triggers immediate resync.next_sync_after(int, optional): Unix timestamp for “Do Not Disturb” feature.
See Protocols and APIs for full response specification.
See Also
Multi-tenancy Architecture - Multi-tenant architecture and configuration
Architecture Overview - Why use an email proxy
FAQ - Troubleshooting and common issues
API Reference - Complete REST API documentation
Protocols and APIs - Message format and delivery reports