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: 1. **Local Persistence** - Write messages to your application's database first 2. **Async Submission** - Submit messages to mail service via REST API 3. **Immediate Trigger** - Optionally trigger ``run-now`` for fast delivery 4. **Delivery Reports** - Receive confirmations via ``proxy_sync`` endpoint 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 ^^^^^^^^^^^^ .. code-block:: bash # 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``: .. code-block:: 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 ^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash curl -X POST http://localhost:8081/send-test-email Response: .. code-block:: json { "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 ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash # Send 5 test emails curl -X POST http://localhost:8081/send-test-email?count=5 Custom Subject Line ^^^^^^^^^^^^^^^^^^^ .. code-block:: bash curl -X POST "http://localhost:8081/send-test-email?subject=Integration%20Test" View Message Statistics ^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash curl http://localhost:8081/stats Response: .. code-block:: json { "status": "success", "statistics": { "total": 10, "pending": 0, "submitted": 2, "delivered": 7, "failed": 1 } } List Pending Messages ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash curl http://localhost:8081/messages Response: .. code-block:: json { "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 ^^^^^^^^^^^^^^^^ .. code-block:: text ┌──────────────────┐ │ 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 /email/mailproxy/mp_endpoint/proxy_sync │ (delivery reports) │ ▼ ┌──────────────────┐ │ Your Application│ │ (example_client)│ │ │ │ 4. Update local │ │ DB status │ └──────────────────┘ Message States ^^^^^^^^^^^^^^ Messages transition through these states in the local database: 1. **pending** - Created locally, not yet submitted to mail service 2. **submitted** - Sent to mail service, awaiting delivery 3. **delivered** - Confirmation received from mail service 4. **failed** - Permanent error reported by mail service Database Schema ^^^^^^^^^^^^^^^ The example client maintains this schema: .. code-block:: sql 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:** .. code-block:: python # BAD: If SMTP server is down, message is lost smtp.send_message(msg) # User's email is gone forever! **❌ Naive proxy approach:** .. code-block:: python # 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):** .. code-block:: python # 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 ^^^^^^^^ 1. **Local-first persistence** - Message committed before network calls 2. **Resilient to downtime** - Mail service can be restarted without data loss 3. **Non-blocking** - Your application doesn't wait for SMTP 4. **Auditable** - Full lifecycle tracked in your database 5. **Testable** - Easy to mock and test 6. **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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python @app.post("/email/mailproxy/mp_endpoint/proxy_sync") async def proxy_sync(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:** .. code-block:: json { "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:** .. code-block:: json { "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:** .. code-block:: json { "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:** .. code-block:: json { "reports": [ { "id": "test_1761215432000_1234", "status": "sent", "timestamp": 1761215435 } ] } **Response:** .. code-block:: json { "status": "ok", "processed": 1 } Testing ------- End-to-End Test ^^^^^^^^^^^^^^^ 1. **Start mail service**: .. code-block:: bash # In terminal 1 cd /path/to/genro-mail-proxy python3 main.py 2. **Start example client**: .. code-block:: bash # In terminal 2 python3 example_client.py 3. **Send test email**: .. code-block:: bash # In terminal 3 curl -X POST http://localhost:8081/send-test-email 4. **Monitor delivery**: .. code-block:: bash # Check example client stats curl http://localhost:8081/stats # Check mail service status curl http://localhost:8000/status 5. **Verify email**: Check your inbox for the test email (configured in ``example_config.ini``). Expected Timeline ^^^^^^^^^^^^^^^^^ .. code-block:: text 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**: .. code-block:: bash pip install fastapi uvicorn aiohttp Mail Service Connection Refused ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Error**: ``Mail service unavailable: Cannot connect to host localhost:8000`` **Solution**: 1. Verify mail service is running: .. code-block:: bash curl http://localhost:8000/status 2. Check ``example_config.ini`` URL is correct: .. code-block:: ini [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 ``config.ini``: .. code-block:: bash # example_config.ini [mail_service] api_token = your-secret-token # config.ini (mail service) [api] token = your-secret-token Messages Stuck in "submitted" State ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Symptoms**: ``curl http://localhost:8081/stats`` shows ``submitted > 0`` for extended period. **Diagnosis**: 1. Check mail service is processing: .. code-block:: bash curl http://localhost:8000/status 2. Check for delivery errors: .. code-block:: bash curl http://localhost:8000/messages | jq '.messages[] | select(.error_ts != null)' 3. Check SMTP account configuration: .. code-block:: bash 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 main `TROUBLESHOOTING.md <../TROUBLESHOOTING.md>`_ for detailed diagnostics. Adapting for Your Application ------------------------------ To integrate this pattern into your application: 1. **Add outbound_emails table** to your existing database schema 2. **Create wrapper function** around your existing email-sending code: .. code-block:: python 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 3. **Add proxy_sync endpoint** to receive delivery reports 4. **Add polling mechanism** (optional, for applications that don't receive run-now triggers) See Also -------- - `Architecture Overview `_ - Why use an email proxy - `TROUBLESHOOTING.md <../TROUBLESHOOTING.md>`_ - Diagnostic procedures - `API Reference `_ - Complete REST API documentation - `Protocol `_ - Message format and delivery reports