API Reference
FastAPI
The REST API is served by FastAPI. The automatically generated documentation is available at:
OpenAPI JSON:
http://localhost:8000/openapi.jsonSwagger UI:
http://localhost:8000/docsReDoc:
http://localhost:8000/redoc
All endpoints require the X-API-Token header when
GMP_API_TOKEN is configured.
Core endpoints
POST /commands/run-nowWake the dispatcher and reporting loops so they execute a cycle immediately instead of waiting for the next scheduled interval.
Behavior depends on authentication token:
Admin token: Processes all pending messages across all tenants. Does not override any tenant’s “Do Not Disturb” (DND) settings.
Tenant token: Only processes that specific tenant. Additionally, resets the tenant’s DND setting, forcing an immediate sync call even if the tenant had previously requested to not be disturbed until a future time.
Use cases:
Admin token: Maintenance scripts, test_mode instances where interval is infinite
Tenant token: Tenant has urgent messages and needs immediate sync, even if they previously set a DND period (e.g., during night hours)
Examples:
# Admin: wake all tenants (respects DND) curl -X POST "http://localhost:8000/commands/run-now" \ -H "X-API-Token: $ADMIN_TOKEN" # Tenant: force immediate sync for this tenant only (overrides DND) curl -X POST "http://localhost:8000/commands/run-now" \ -H "X-API-Token: $TENANT_TOKEN"
POST /commands/suspend/POST /commands/activateToggle the scheduler.
POST /commands/add-messagesValidate and enqueue a batch of messages. Each payload entry matches
mail_proxy.api.MessagePayload; the response contains the number of queued items plus arejectedlist with{"id","reason"}entries describing invalid payloads (missingid, bad addresses, unknown account, duplicates, …). Seecore.mail_proxy.entities.message.endpoint.MessageEndpoint.POST /account/GET /accounts/DELETE /account/{id}Manage SMTP account credentials.
GET /messagesInspect the SQLite-backed
messagestable. Each record includes:pk: Internal UUID primary keyid: Client-provided message identifiertenant_id: Tenant identifiertenant_name: Tenant display name (from tenants table)account_id: SMTP account used for sendingpriority: Message priority (1-4)payload: Original message contentLifecycle fields:
deferred_ts,smtp_ts,error_ts,error
GET /metricsExpose Prometheus metrics generated by
tools.prometheus.metrics.MailMetrics.Example:
curl "http://localhost:8000/metrics" \ -H "X-API-Token: your-token"
Tenant management
These endpoints manage tenant configurations for multi-tenant deployments. See Multi-tenancy Architecture for full architecture details.
POST /tenantCreate or update a tenant configuration.
Example:
curl -X POST "http://localhost:8000/tenant" \ -H "X-API-Token: your-token" \ -H "Content-Type: application/json" \ -d '{ "id": "tenant-acme", "name": "ACME Corporation", "client_base_url": "https://api.acme.com", "client_sync_path": "/mail-proxy/sync", "active": true }'
Request body:
{ "id": "tenant-acme", "name": "ACME Corporation", "client_base_url": "https://api.acme.com", "client_sync_path": "/mail-proxy/sync", "client_attachment_path": "/mail-proxy/attachments", "client_auth": { "method": "bearer", "token": "secret-token" }, "rate_limits": { "hourly": 1000, "daily": 10000 }, "large_file_config": { "enabled": true, "max_size_mb": 10, "storage_url": "s3://bucket/mail-attachments", "file_ttl_days": 30, "action": "rewrite" }, "active": true }
large_file_config fields:
enabled(bool): Enable large file handling (default: false)max_size_mb(float): Size threshold in MB (default: 10.0)storage_url(string): fsspec URL for storage backend: - S3/MinIO:s3://bucket/path- Google Cloud Storage:gs://bucket/path- Azure Blob:az://container/path- Local filesystem:file:///var/www/downloadspublic_base_url(string): Required for local filesystem storagefile_ttl_days(int): Days before files expire (default: 30)lifespan_after_download_days(int): Days to keep after first downloadaction(string): Behavior when limit exceeded: -warn: Log warning, send normally (default) -reject: Reject message with error -rewrite: Upload to storage, replace with download link
Response:
{"ok": true}GET /tenantsList all configured tenants.
Query parameters:
active_only(bool, optional): Filter to active tenants only
Response:
{ "ok": true, "tenants": [ { "id": "tenant-acme", "name": "ACME Corporation", "client_base_url": "https://api.acme.com", "active": true, "created_at": "2024-01-20T10:00:00Z" } ] }
GET /tenants/sync-statusRetrieve synchronization status for all tenants (admin only).
Returns the last sync timestamp and Do Not Disturb status for each tenant. Useful for monitoring tenant synchronization health.
Response:
{ "ok": true, "sync_interval_seconds": 300, "tenants": [ { "id": "tenant-acme", "name": "ACME Corporation", "active": true, "client_base_url": "https://api.acme.com", "last_sync_ts": 1706450400, "next_sync_due": false, "in_dnd": false }, { "id": "tenant-idle", "name": "Idle Corp", "active": true, "client_base_url": "https://idle.example.com", "last_sync_ts": 1706500800, "next_sync_due": false, "in_dnd": true } ] }
Response fields per tenant:
last_sync_ts: Unix timestamp of last sync call (or future timestamp if DND)next_sync_due:trueif sync interval has expired and tenant should be calledin_dnd:trueif tenant is in “Do Not Disturb” mode (futurelast_sync_ts)
GET /tenant/{tenant_id}Get a specific tenant configuration.
Response: Single tenant object or
404if not found.PUT /tenant/{tenant_id}Update an existing tenant. All fields are optional.
Response:
{"ok": true}DELETE /tenant/{tenant_id}Remove a tenant configuration.
Response:
{"ok": true}
Additional commands
POST /commands/delete-messages?tenant_id=<tenant_id>Remove messages from the queue by their IDs.
Query parameters:
tenant_id(string, required): Tenant identifier for security isolation
Request body:
{ "ids": ["MSG-001", "MSG-002"] }
Example:
curl -X POST "http://localhost:8000/commands/delete-messages?tenant_id=acme" \ -H "X-API-Token: your-token" \ -H "Content-Type: application/json" \ -d '{"ids": ["MSG-001", "MSG-002"]}'
Response:
{ "ok": true, "removed": 2, "not_found": [], "unauthorized": [] }
The
unauthorizedfield contains IDs of messages that belong to a different tenant.POST /commands/cleanup-messages?tenant_id=<tenant_id>Remove old reported messages based on retention policy.
Query parameters:
tenant_id(string, required): Tenant identifier for security isolation
Request body (optional):
{ "older_than_seconds": 86400 }
If
older_than_secondsis not provided, uses the configuredreport_retention_secondsvalue.Example:
# With custom retention period curl -X POST "http://localhost:8000/commands/cleanup-messages?tenant_id=acme" \ -H "X-API-Token: your-token" \ -H "Content-Type: application/json" \ -d '{"older_than_seconds": 86400}' # With default retention curl -X POST "http://localhost:8000/commands/cleanup-messages?tenant_id=acme" \ -H "X-API-Token: your-token" \ -H "Content-Type: application/json" \ -d '{}'
Response:
{ "ok": true, "removed": 42 }
Prometheus metrics
The following metrics are exported (all prefixed with gmp_ for genro-mail-proxy):
gmp_sent_total{account_id}- Total successfully sent emailsgmp_errors_total{account_id}- Total permanent send failuresgmp_deferred_total{account_id}- Total temporarily deferred messagesgmp_rate_limited_total{account_id}- Total rate limit enforcement eventsgmp_pending_messages- Current queue depth
Outbound proxy sync
Besides REST endpoints that clients call, the service also issues a
POST request to the configured sync endpoint (per-tenant: client_base_url +
client_sync_path, or global: GMP_CLIENT_SYNC_URL) whenever there are
delivery results to share with your application. Example payload:
The payload contains the delivery results read from the messages table:
{
"delivery_report": [
{
"tenant_id": "tenant-acme",
"id": "MSG-101",
"pk": "550e8400-e29b-41d4-a716-446655440000",
"sent_ts": 1728458400
},
{
"tenant_id": "tenant-acme",
"id": "MSG-102",
"pk": "550e8400-e29b-41d4-a716-446655440001",
"error_ts": 1728458612,
"error": "SMTP timeout"
}
]
}
Each report includes tenant_id, id (client-facing), and pk (internal UUID).
Event-specific fields depend on the event type: sent_ts for successful delivery,
error_ts and error for failures, deferred_ts for retries.
Client Callback Endpoints
These endpoints are NOT exposed by the proxy. Your application must implement them to receive callbacks from the proxy.
Sync Endpoint (Required)
The proxy sends delivery reports to your sync endpoint (per-tenant: client_base_url
+ client_sync_path, or global: GMP_CLIENT_SYNC_URL). This is a bidirectional
protocol that allows you to both receive reports AND signal pending messages.
Request (from proxy to your application):
{
"delivery_report": [
{"id": "MSG-001", "sent_ts": 1728458400},
{"id": "MSG-002", "error_ts": 1728458612, "error": "SMTP timeout"},
{"id": "MSG-003", "pec_event": "pec_acceptance", "pec_ts": 1728458700}
]
}
Response schema (from your application to proxy):
{
"ok": true,
"queued": 15,
"error": [],
"not_found": []
}
Response fields:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
bool |
Yes |
|
|
int |
No (default: 0) |
Number of messages your application has ready to send. Important: When
|
|
int |
No |
Unix timestamp. Proxy will not sync this tenant until this time (“Do Not
Disturb”). Useful for serverless databases to avoid cold-start costs during
idle hours. Tenant can override by calling |
|
list[str] |
No |
Message IDs that could not be processed |
|
list[str] |
No |
Message IDs not found in your database |
Accelerated sync loop:
When your application responds with queued > 0:
Proxy receives response with
queued: NYour application calls
POST /commands/add-messageswith next batchProxy immediately calls sync endpoint again (no wait)
Repeat until
queued: 0
This design allows efficient batch submission without polling.
Example implementation:
@app.post("/proxy-sync")
async def sync_endpoint(request: Request):
data = await request.json()
reports = data.get("delivery_report", [])
# 1. Process delivery reports
for report in reports:
msg_id = report["id"]
if "sent_ts" in report:
mark_sent(msg_id, report["sent_ts"])
elif "error_ts" in report:
mark_failed(msg_id, report["error"])
elif "pec_event" in report:
handle_pec(msg_id, report["pec_event"], report.get("pec_ts"))
# 2. Check outbox and submit next batch
pending = get_pending_messages(limit=100)
total_pending = count_all_pending()
if pending:
# Submit batch to proxy (async, don't block this response)
asyncio.create_task(submit_to_proxy(pending))
# 3. Return queued count to trigger immediate resync if needed
return {"ok": True, "queued": total_pending}
See Protocols and APIs for the complete specification.
Attachment Endpoint (optional)
If using fetch_mode: "endpoint" for attachments, the proxy fetches file content
from your attachment endpoint (client_base_url + client_attachment_path).
Your endpoint receives a POST with the storage_path value and must return the
file content.
Example implementation:
@app.post("/attachments")
async def serve_attachment(request: Request):
data = await request.json()
storage_path = data.get("storage_path") # e.g., "doc_id=123"
# Parse storage_path and fetch file from your storage
file_content = fetch_file_from_storage(storage_path)
return Response(content=file_content, media_type="application/octet-stream")