# Mailcow Logs Viewer - API Documentation This document describes all available API endpoints for the Mailcow Logs Viewer application. **Base URL:** `http://your-server:8080/api` **Authentication:** When `AUTH_ENABLED=true`, all API endpoints (except `/api/health`) require HTTP Basic Authentication. Include the `Authorization: Basic ` header in all requests. --- ## Table of Contents 1. [Authentication](#authentication) 2. [Health & Info](#health--info) 3. [Job Status Tracking](#job-status-tracking) 4. [Domains](#domains) 5. [Mailbox Statistics](#mailbox-statistics) 6. [Messages (Unified View)](#messages-unified-view) 7. [Logs](#logs) - [Postfix Logs](#postfix-logs) - [Rspamd Logs](#rspamd-logs) - [Netfilter Logs](#netfilter-logs) 8. [Queue & Quarantine](#queue--quarantine) 9. [Statistics](#statistics) 10. [Status](#status) 11. [Settings](#settings) - [SMTP & IMAP Test](#smtp--imap-test) 12. [Export](#export) 13. [DMARC](#dmarc) - [DMARC IMAP Auto-Import](#dmarc-imap-auto-import) --- ## Authentication ### Overview When authentication is enabled (`AUTH_ENABLED=true`), all API endpoints except `/api/health` require HTTP Basic Authentication. **Public Endpoints (No Authentication Required):** - `GET /api/health` - Health check (for Docker monitoring) - `GET /login` - Login page (HTML) **Protected Endpoints (Authentication Required):** - All other `/api/*` endpoints ### Authentication Method Use HTTP Basic Authentication with the credentials configured in your environment: - Username: `AUTH_USERNAME` (default: `admin`) - Password: `AUTH_PASSWORD` **Example Request:** ```bash curl -u username:password http://your-server:8080/api/info ``` Or with explicit header: ```bash curl -H "Authorization: Basic $(echo -n 'username:password' | base64)" \ http://your-server:8080/api/info ``` ### Login Endpoint #### GET /login Serves the login page (HTML). This endpoint is always publicly accessible. **Response:** HTML page with login form **Note:** When authentication is disabled, accessing this endpoint will automatically redirect to the main application. --- ## Health & Info ### GET /health Health check endpoint for monitoring and load balancers. **Authentication:** Not required (public endpoint for Docker health checks) **Response:** ```json { "status": "healthy", "database": "connected", "version": "1.5.0", "config": { "fetch_interval": 60, "retention_days": 7, "mailcow_url": "https://mail.example.com", "blacklist_enabled": true, "auth_enabled": false } } ``` --- ### GET /info Application information and configuration. **Response:** ```json { "name": "Mailcow Logs Viewer", "version": "1.5.0", "mailcow_url": "https://mail.example.com", "local_domains": ["example.com", "mail.example.com"], "fetch_interval": 60, "retention_days": 7, "timezone": "UTC", "app_title": "Mailcow Logs Viewer", "app_logo_url": "", "blacklist_count": 3, "auth_enabled": false } ``` --- ## Job Status Tracking ### Overview The application includes a real-time job status tracking system that monitors all background jobs. Each job reports its execution status, timestamp, and any errors that occurred. ### Job Status Data Structure ```python job_status = { 'fetch_logs': {'last_run': datetime, 'status': str, 'error': str|None}, 'complete_correlations': {'last_run': datetime, 'status': str, 'error': str|None}, 'update_final_status': {'last_run': datetime, 'status': str, 'error': str|None}, 'expire_correlations': {'last_run': datetime, 'status': str, 'error': str|None}, 'cleanup_logs': {'last_run': datetime, 'status': str, 'error': str|None}, 'check_app_version': {'last_run': datetime, 'status': str, 'error': str|None}, 'dns_check': {'last_run': datetime, 'status': str, 'error': str|None}, 'update_geoip': {'last_run': datetime, 'status': str, 'error': str|None} } ``` ### Status Values | Status | Description | Badge Color | |--------|-------------|-------------| | `running` | Job is currently executing | Blue (bg-blue-500) | | `success` | Job completed successfully | Green (bg-green-600) | | `failed` | Job encountered an error | Red (bg-red-600) | | `idle` | Job hasn't run yet | Gray (bg-gray-500) | | `scheduled` | Job is scheduled but runs infrequently | Purple (bg-purple-600) | ### Accessing Job Status Job status is accessible through: 1. **Backend Function**: `get_job_status()` in `scheduler.py` 2. **API Endpoint**: `GET /api/settings/info` (includes `background_jobs` field) 3. **Frontend Display**: Settings page > Background Jobs section ### Background Jobs List | Job Name | Interval | Description | |----------|----------|-------------| | **Fetch Logs** | 60 seconds | Imports Postfix, Rspamd, and Netfilter logs from Mailcow API | | **Complete Correlations** | 120 seconds (2 min) | Links Postfix logs to message correlations | | **Update Final Status** | 120 seconds (2 min) | Updates message delivery status for late-arriving logs | | **Expire Correlations** | 60 seconds (1 min) | Marks old incomplete correlations as expired (after 10 minutes) | | **Cleanup Logs** | Daily at 2 AM | Removes logs older than retention period | | **Check App Version** | 6 hours | Checks GitHub for application updates | | **DNS Check** | 6 hours | Validates DNS records (SPF, DKIM, DMARC) for all active domains | | **Update GeoIP** | Weekly (Sunday 3 AM) | Updates MaxMind GeoIP databases for DMARC source IP enrichment | ### Implementation Details **Update Function:** ```python def update_job_status(job_name: str, status: str, error: str = None): """Update job execution status""" job_status[job_name] = { 'last_run': datetime.now(timezone.utc), 'status': status, 'error': error } ``` **Usage in Jobs:** ```python async def some_background_job(): try: update_job_status('job_name', 'running') # ... job logic ... update_job_status('job_name', 'success') except Exception as e: update_job_status('job_name', 'failed', str(e)) ``` **UI Display:** - Compact card layout with status badges - Icon indicators (⏱ ⏳ 📅 🗂 📋) - Last run timestamp always visible - Error messages displayed in red alert boxes - Pending items count for correlation jobs --- ## Domains ### GET /api/domains/all Get list of all domains with statistics and cached DNS validation results. **Response:** ```json { "total": 10, "active": 8, "last_dns_check": "2026-01-08T01:34:08Z", "domains": [ { "domain_name": "example.com", "active": true, "mboxes_in_domain": 5, "mboxes_left": 995, "max_num_mboxes_for_domain": 1000, "aliases_in_domain": 3, "aliases_left": 397, "max_num_aliases_for_domain": 400, "created": "2025-01-01T00:00:00Z", "bytes_total": 1572864, "msgs_total": 1234, "quota_used_in_domain": "1572864", "max_quota_for_domain": 10240000, "backupmx": false, "relay_all_recipients": false, "relay_unknown_only": false, "dns_checks": { "spf": { "status": "success", "message": "SPF configured correctly with strict -all policy. Server IP authorized via ip4:1.2.3.4", "record": "v=spf1 mx include:_spf.google.com -all", "has_strict_all": true, "includes_mx": true, "includes": ["_spf.google.com"], "warnings": [], "dns_lookups": 3 }, "dkim": { "status": "success", "message": "DKIM configured correctly", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;p=MIIBIjANBg...", "actual_record": "v=DKIM1;k=rsa;p=MIIBIjANBg...", "match": true, "warnings": [], "info": [], "parameters": { "v": "DKIM1", "k": "rsa", "p": "MIIBIjANBg..." } }, "dmarc": { "status": "success", "message": "DMARC configured with strict policy", "record": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com", "policy": "reject", "subdomain_policy": null, "pct": "100", "is_strong": true, "warnings": [] }, "checked_at": "2026-01-08T01:34:08Z" } } ] } ``` **Response Fields:** - `total`: Total number of domains - `active`: Number of active domains - `last_dns_check`: Timestamp of last global DNS check (only updated by scheduled or manual full checks) - `domains`: Array of domain objects **Domain Object Fields:** - `domain_name`: Domain name - `active`: Boolean indicating if domain is active - `mboxes_in_domain`: Number of mailboxes - `mboxes_left`: Available mailbox slots - `max_num_mboxes_for_domain`: Maximum mailboxes allowed - `aliases_in_domain`: Number of aliases - `aliases_left`: Available alias slots - `max_num_aliases_for_domain`: Maximum aliases allowed - `created`: Domain creation timestamp (UTC) - `bytes_total`: Total storage used (bytes) - `msgs_total`: Total messages - `quota_used_in_domain`: Storage quota used (string format) - `max_quota_for_domain`: Maximum storage quota - `backupmx`: Boolean - true if domain is backup MX - `relay_all_recipients`: Boolean - true if relaying all recipients - `relay_unknown_only`: Boolean - true if relaying only unknown recipients - `dns_checks`: DNS validation results (cached from database) **DNS Check Status Values:** - `success`: Check passed with no issues - `warning`: Check passed but with recommendations for improvement - `error`: Check failed or record not found - `unknown`: Check not yet performed **SPF Status Indicators:** - **DNS Lookup Limit**: Error if >10 lookups (RFC 7208) - **Server IP Authorization**: Error if mail server IP not found in SPF - **Multiple Records**: Error (only one SPF record allowed per domain) - **Invalid Syntax**: Error (must start with `v=spf1 ` with space) - **Invalid Mechanisms**: Error (only valid mechanisms allowed) - `-all`: Strict policy (status: success) - `~all`: Soft fail (status: success, informational) - `?all`: Neutral (status: warning) - Provides minimal protection - `+all`: Pass all (status: error) - Provides no protection - Missing `all`: No policy defined (status: error) **New SPF Fields:** - `dns_lookups`: Integer count of DNS lookups (0-999) - `warnings`: Array of warning messages **DKIM Validation:** - Fetches expected DKIM record from Mailcow API - Queries DNS for actual DKIM record - Compares expected vs actual records - `match`: Boolean indicating if records match - **Parameter Validation**: Checks for security issues - `t=y` (Testing mode): Critical error - `t=s` (Strict subdomain): Informational only - `h=sha1` (Weak hash): Warning - `p=` (Empty key): Error - key revoked - Unknown key types: Warning **New DKIM Fields:** - `warnings`: Array of security warnings (with icons: ❌ ⚠️) - `info`: Array of informational messages (plain text) - `parameters`: Dictionary of parsed DKIM tags (v, k, t, h, p, etc.) **DMARC Policy Types:** - `reject`: Strict policy (status: success) - `quarantine`: Moderate policy (status: warning) - Consider upgrading to reject - `none`: Monitor only (status: warning) - Provides no protection **Notes:** - DNS checks are cached in database for performance - `last_dns_check` only updates from global/scheduled checks, not individual domain checks - `checked_at` (per domain) updates whenever that specific domain is checked - All timestamps include UTC timezone indicator ('Z' suffix) --- ### POST /api/domains/check-all-dns Manually trigger DNS validation for all active domains. **Description:** Performs DNS checks (SPF, DKIM, DMARC) for all active domains and updates the global `last_dns_check` timestamp. Results are cached in database. **Authentication:** Required **Response:** ```json { "status": "success", "message": "Checked 8 domains", "domains_checked": 8, "errors": [] } ``` **Response Fields:** - `status`: `success` (all domains checked) or `partial` (some domains failed) - `message`: Summary message - `domains_checked`: Number of domains successfully checked - `errors`: Array of error messages for failed domains (empty if all successful) **Error Response (partial success):** ```json { "status": "partial", "message": "Checked 7 domains", "domains_checked": 7, "errors": [ "example.com: DNS timeout" ] } ``` **Notes:** - Only checks active domains - Updates `is_full_check=true` flag in database - Updates global `last_dns_check` timestamp - Frontend shows progress with toast notifications - Returns immediately with status (check runs asynchronously) --- ### POST /api/domains/{domain}/check-dns Manually trigger DNS validation for a specific domain. **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `domain` | string | Domain name to check | **Authentication:** Required **Example Request:** ``` POST /api/domains/example.com/check-dns ``` **Response:** ```json { "status": "success", "message": "DNS checked for example.com", "data": { "domain": "example.com", "spf": { "status": "success", "message": "SPF configured correctly with strict -all policy. Server IP authorized via ip4:1.2.3.4", "record": "v=spf1 mx include:_spf.google.com -all", "has_strict_all": true, "includes_mx": true, "includes": ["_spf.google.com"], "warnings": [], "dns_lookups": 3 }, "dkim": { "status": "success", "message": "DKIM configured correctly", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;p=MIIBIjANBg...", "actual_record": "v=DKIM1;k=rsa;p=MIIBIjANBg...", "match": true, "warnings": [], "info": [], "parameters": { "v": "DKIM1", "k": "rsa", "p": "MIIBIjANBg..." } }, "dmarc": { "status": "success", "message": "DMARC configured with strict policy", "record": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com", "policy": "reject", "is_strong": true, "warnings": [] }, "checked_at": "2026-01-08T01:45:23Z" } } ``` **Notes:** - Only checks the specified domain - Updates `is_full_check=false` flag in database - Does NOT update global `last_dns_check` timestamp - Frontend updates only that domain's section (no page refresh) - Useful for verifying DNS changes immediately --- ### DNS Check Technical Details **Async DNS Validation:** - All DNS queries use async resolvers with 5-second timeout - Queries run in parallel for performance - Comprehensive error handling for timeouts, NXDOMAIN, NoAnswer **SPF Validation:** - Queries TXT records for SPF (`v=spf1`) - Validates syntax and structure: - Checks for multiple SPF records (RFC violation) - Validates `v=spf1` with space after - Checks for valid mechanisms only (ip4, ip6, a, mx, include, exists, all) - Validates presence of `all` mechanism - Detects policy: `-all`, `~all`, `?all`, `+all`, or missing - Checks for `mx` mechanism - Extracts `include:` directives - **DNS Lookup Counter** (RFC 7208 compliance): - Recursively counts DNS lookups through includes - Counts `a`, `mx`, `exists:`, `redirect=`, and `include:` mechanisms - Maximum 10 lookups enforced (returns error if exceeded) - Returns `dns_lookups` field with count - **Server IP Authorization**: - Fetches server IP from Mailcow API once on startup - Verifies server IP is authorized via: - Direct `ip4:` match (including CIDR ranges) - `a` record resolution - `mx` record resolution - Recursive `include:` checking (up to 10 levels) - Returns authorization method in message (e.g., "Server IP authorized via ip4:X.X.X.X") - Returns error if server IP not found in SPF record - Provides policy-specific warnings and recommendations **DKIM Validation:** - Fetches expected DKIM value from Mailcow API (`/api/v1/get/dkim/{domain}`) - Queries DNS at `{selector}._domainkey.{domain}` - Compares expected vs actual records (whitespace-normalized) - **Parameter Validation**: - Parses all DKIM tags (v, k, t, h, p, etc.) - **Testing Mode Detection** (`t=y`): Returns critical error - Warning: "Emails will pass validation even with invalid signatures" - Never use in production - **Strict Subdomain Mode** (`t=s`): Returns informational message - Only main domain can send, subdomains will fail DKIM - Does NOT affect validation status (remains "success") - **Revoked Key Detection** (`p=` empty): Returns error - Indicates DKIM has been intentionally disabled - **Weak Hash Algorithm** (`h=sha1`): Returns warning - Recommends upgrade to SHA256 - **Key Type Validation** (`k=`): Validates rsa or ed25519 - Returns three arrays: - `warnings`: Security issues (errors and warnings with icons) - `info`: Informational messages (plain text, no status impact) - `parameters`: Parsed DKIM parameter dictionary - Reports mismatch details **DMARC Validation:** - Queries TXT records at `_dmarc.{domain}` - Parses policy (`p=` tag) - Checks for subdomain policy (`sp=` tag) - Validates percentage (`pct=` tag) - Provides policy upgrade recommendations **Background Checks:** - Automated DNS checks run every 6 hours via scheduler - Only checks active domains - All automated checks marked as `is_full_check=true` - Results cached in `domain_dns_checks` table **Caching:** - DNS results stored in PostgreSQL with JSONB columns - Indexed on `domain_name` and `checked_at` for performance - Upsert pattern (update if exists, insert if new) - `is_full_check` flag distinguishes check types --- ### DNS Validation Examples #### SPF Examples **Example 1: Too Many DNS Lookups** ```json { "status": "error", "message": "SPF has too many DNS lookups (11). Maximum is 10", "record": "v=spf1 include:_spf.exmail.email -all", "has_strict_all": true, "includes_mx": false, "includes": ["_spf.exmail.email"], "warnings": [ "SPF record exceeds the 10 DNS lookup limit with 11 lookups", "This will cause SPF validation to fail" ], "dns_lookups": 11 } ``` **Example 2: Server IP Not Authorized** ```json { "status": "error", "message": "Server IP 1.2.3.4 is NOT authorized in SPF record", "record": "v=spf1 ip4:1.2.3.4 -all", "has_strict_all": true, "includes_mx": false, "includes": [], "warnings": [ "Mail server IP not found in SPF record" ], "dns_lookups": 0 } ``` **Example 3: Multiple SPF Records** ```json { "status": "error", "message": "Multiple SPF records found (2). Only one is allowed", "record": "v=spf1 mx -all; v=spf1 ip4:1.2.3.4 -all", "has_strict_all": false, "includes_mx": false, "includes": [], "warnings": [ "Multiple SPF records invalidate ALL records" ] } ``` **Example 4: Success with Server IP Authorization** ```json { "status": "success", "message": "SPF configured correctly with strict -all policy. Server IP authorized via include:_spf.google.com (ip4:1.2.3.4)", "record": "v=spf1 include:_spf.google.com -all", "has_strict_all": true, "includes_mx": false, "includes": ["_spf.google.com"], "warnings": [], "dns_lookups": 3 } ``` #### DKIM Examples **Example 1: Testing Mode (Critical)** ```json { "status": "error", "message": "DKIM is in TESTING mode (t=y) - Emails will pass validation even with invalid signatures. Remove t=y for production!", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;t=y;p=MIIBIjANBg...", "actual_record": "v=DKIM1;k=rsa;t=y;p=MIIBIjANBg...", "match": true, "warnings": [], "info": [], "parameters": { "v": "DKIM1", "k": "rsa", "t": "y", "p": "MIIBIjANBg..." } } ``` **Example 2: Strict Subdomain Mode (Informational)** ```json { "status": "success", "message": "DKIM configured correctly", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;t=s;p=MIIBIjANBg...", "actual_record": "v=DKIM1;k=rsa;t=s;p=MIIBIjANBg...", "match": true, "warnings": [], "info": [ "DKIM uses strict subdomain mode (t=s)" ], "parameters": { "v": "DKIM1", "k": "rsa", "t": "s", "p": "MIIBIjANBg..." } } ``` **Example 3: SHA1 Warning** ```json { "status": "warning", "message": "DKIM configured but has warnings", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;h=sha1;p=MIIBIjANBg...", "actual_record": "v=DKIM1;k=rsa;h=sha1;p=MIIBIjANBg...", "match": true, "warnings": [ "⚠️ DKIM uses SHA1 hash algorithm (h=sha1)" ], "info": [], "parameters": { "v": "DKIM1", "k": "rsa", "h": "sha1", "p": "MIIBIjANBg..." } } ``` **Example 4: Revoked Key** ```json { "status": "error", "message": "DKIM key is revoked (p= is empty)", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;p=", "actual_record": "v=DKIM1;k=rsa;p=", "match": true, "warnings": [ "❌ DKIM key is revoked (p= is empty)" ], "info": [], "parameters": { "v": "DKIM1", "k": "rsa", "p": "" } } ``` **Example 5: Multiple Issues** ```json { "status": "warning", "message": "DKIM configured but has warnings", "selector": "dkim", "dkim_domain": "dkim._domainkey.example.com", "expected_record": "v=DKIM1;k=rsa;t=s;h=sha1;p=MIIBIjANBg...", "actual_record": "v=DKIM1;k=rsa;t=s;h=sha1;p=MIIBIjANBg...", "match": true, "warnings": [ "⚠️ DKIM uses SHA1 hash algorithm (h=sha1)" ], "info": [ "DKIM uses strict subdomain mode (t=s)" ], "parameters": { "v": "DKIM1", "k": "rsa", "t": "s", "h": "sha1", "p": "MIIBIjANBg..." } } ``` --- ## Mailbox Statistics ### GET /api/mailbox-stats/summary Get summary statistics for all mailboxes. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `date_range` | string | Date range: `today`, `7days`, `30days`, `90days` (default: `30days`) | **Response:** ```json { "total_mailboxes": 25, "active_mailboxes": 23, "inactive_mailboxes": 2, "total_sent": 1234, "total_received": 5678, "sent_failed": 45, "failure_rate": 3.6, "date_range": "30days", "start_date": "2026-01-16T00:00:00Z", "end_date": "2026-02-16T00:00:00Z" } ``` --- ### GET /api/mailbox-stats/all Get all mailbox statistics with message counts and aliases (paginated). **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `domain` | string | Filter by domain name | | `active_only` | bool | Only show active mailboxes (default: `true`) | | `hide_zero` | bool | Hide mailboxes with zero activity (default: `false`) | | `search` | string | Search mailbox username, name, or alias address | | `date_range` | string | Date range: `today`, `7days`, `30days`, `90days` (default: `30days`) | | `sort_by` | string | Sort by: `sent_total`, `received_total`, `failure_rate`, `quota_used`, `username` | | `sort_order` | string | Sort order: `asc`, `desc` (default: `desc`) | | `page` | int | Page number (default: `1`) | | `page_size` | int | Items per page, 10-100 (default: `50`) | **Example Request:** ``` GET /api/mailbox-stats/all?date_range=30days&active_only=true&hide_zero=true&sort_by=sent_total&sort_order=desc&page=1 ``` **Response:** ```json { "total": 25, "page": 1, "page_size": 50, "total_pages": 1, "date_range": "30days", "start_date": "2026-01-16T00:00:00Z", "end_date": "2026-02-16T00:00:00Z", "mailboxes": [ { "id": 1, "username": "user@example.com", "domain": "example.com", "name": "John Doe", "active": true, "quota": 1073741824, "quota_formatted": "1.0 GB", "quota_used": 536870912, "quota_used_formatted": "512 MB", "percent_in_use": 50.0, "messages_in_mailbox": 1234, "last_imap_login": "2026-01-15T10:30:00Z", "last_pop3_login": null, "last_smtp_login": "2026-01-16T08:45:00Z", "rl_value": 100, "rl_frame": "m", "attributes": { "imap_access": "1", "pop3_access": "0", "smtp_access": "1", "sieve_access": "1", "sogo_access": "1", "tls_enforce_in": "0", "tls_enforce_out": "0" }, "mailbox_counts": { "sent_total": 150, "sent_delivered": 145, "sent_bounced": 3, "sent_deferred": 2, "sent_rejected": 0, "sent_failed": 5, "received_total": 320, "failure_rate": 3.3 }, "aliases": [ { "alias_address": "info@example.com", "active": true, "is_catch_all": false, "sent_total": 50, "sent_delivered": 48, "sent_bounced": 2, "sent_deferred": 0, "sent_rejected": 0, "sent_failed": 2, "received_total": 100, "failure_rate": 4.0 } ], "alias_count": 1, "combined_sent": 200, "combined_received": 420, "combined_total": 620, "combined_failed": 7, "combined_failure_rate": 3.5, "created": "2025-01-01T00:00:00Z", "modified": "2026-01-15T12:00:00Z" } ] } ``` **Response Fields:** | Field | Description | |-------|-------------| | `username` | Email address of the mailbox | | `name` | Display name | | `active` | Whether mailbox is active in Mailcow | | `quota` / `quota_used` | Quota in bytes | | `percent_in_use` | Quota usage percentage | | `messages_in_mailbox` | Number of messages stored | | `last_*_login` | Last login timestamps (null if never) | | `rl_value` / `rl_frame` | Rate limiting (e.g., 100/m = 100 per minute) | | `attributes` | Access permissions from Mailcow | | `mailbox_counts` | Message statistics for mailbox only | | `aliases` | Array of alias statistics | | `combined_*` | Combined totals (mailbox + all aliases) | | `created` / `modified` | Mailbox creation and last update timestamps | --- ### GET /api/mailbox-stats/domains Get list of domains with mailbox counts for filter dropdown. **Response:** ```json { "domains": [ { "domain": "example.com", "mailbox_count": 15 }, { "domain": "company.org", "mailbox_count": 10 } ] } ``` ### Caching The Mailbox Statistics API uses in-memory caching to improve performance: | Setting | Value | |---------|-------| | **Cache TTL** | 5 minutes (300 seconds) | | **Cache Scope** | Per unique query parameter combination | | **Cached Parameters** | domain, active_only, hide_zero, search, date_range, start_date, end_date, sort_by, sort_order, page, page_size | **Cache Behavior:** - First request with specific parameters fetches from database and caches result - Subsequent requests with identical parameters return cached data - Cache automatically expires after 5 minutes - Changing any parameter results in a cache miss (new database query) **Cache Management:** ```python from app.routers.mailbox_stats import clear_stats_cache # Clear all stats cache (e.g., after data import) clear_stats_cache() ``` --- ## Messages (Unified View) ### GET /messages Get unified messages view combining Postfix and Rspamd data. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `page` | int | Page number (default: 1) | | `limit` | int | Items per page (default: 50, max: 500) | | `search` | string | Search in sender, recipient, subject, message_id, queue_id | | `sender` | string | Filter by sender email | | `recipient` | string | Filter by recipient email | | `direction` | string | Filter by direction: `inbound`, `outbound`, `internal` | | `status` | string | Filter by status: `delivered`, `bounced`, `deferred`, `rejected`, `spam`
**Note:** `spam` filter checks both `final_status='spam'` and `is_spam=True` from Rspamd | | `user` | string | Filter by authenticated user | | `ip` | string | Filter by source IP address | | `start_date` | datetime | Start date (ISO format) | | `end_date` | datetime | End date (ISO format) | **Example Request:** ``` GET /api/messages?page=1&limit=50&direction=outbound&sender=user@example.com ``` **Response:** ```json { "total": 1234, "page": 1, "limit": 50, "pages": 25, "data": [ { "correlation_key": "abc123def456...", "message_id": "", "queue_id": "ABC123DEF", "sender": "user@example.com", "recipient": "recipient@gmail.com", "subject": "Hello World", "direction": "outbound", "final_status": "delivered", "is_complete": true, "first_seen": "2025-12-25T10:30:00Z", "last_seen": "2025-12-25T10:30:05Z", "spam_score": 0.5, "is_spam": false, "user": "user@example.com", "ip": "192.168.1.100" } ] } ``` --- ### GET /message/{correlation_key}/details Get complete message details with all related logs. **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `correlation_key` | string | The correlation key (SHA256 hash) | **Response:** ```json { "correlation_key": "abc123def456...", "message_id": "", "queue_id": "ABC123DEF", "sender": "user@example.com", "recipient": "recipient@gmail.com", "recipients": ["recipient@gmail.com", "cc@gmail.com"], "recipient_count": 2, "subject": "Hello World", "direction": "outbound", "final_status": "delivered", "is_complete": true, "first_seen": "2025-12-25T10:30:00Z", "last_seen": "2025-12-25T10:30:05Z", "rspamd": { "time": "2025-12-25T10:30:00Z", "score": 0.5, "required_score": 15, "action": "no action", "symbols": { "MAILCOW_AUTH": {"score": -20, "description": "mailcow authenticated"}, "RCVD_COUNT_ZERO": {"score": 0, "options": ["0"]} }, "is_spam": false, "direction": "outbound", "ip": "192.168.1.100", "user": "user@example.com", "has_auth": true, "size": 1024 }, "postfix": [ { "time": "2025-12-25T10:30:00Z", "program": "postfix/smtpd", "priority": "info", "message": "ABC123DEF: client=...", "status": null, "relay": null, "delay": null, "dsn": null }, { "time": "2025-12-25T10:30:05Z", "program": "postfix/smtp", "priority": "info", "message": "ABC123DEF: to=, relay=gmail-smtp-in.l.google.com...", "status": "sent", "relay": "gmail-smtp-in.l.google.com[142.251.168.26]:25", "delay": 1.5, "dsn": "2.0.0" } ], "postfix_by_recipient": { "recipient@gmail.com": [...], "cc@gmail.com": [...], "_system": [...] }, "netfilter": [] } ``` --- ## Logs ### Postfix Logs #### GET /logs/postfix Get Postfix logs grouped by Queue-ID. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `page` | int | Page number (default: 1) | | `limit` | int | Items per page (default: 50, max: 500) | | `search` | string | Search in message, sender, recipient, queue_id | | `sender` | string | Filter by sender | | `recipient` | string | Filter by recipient | | `status` | string | Filter by status: `sent`, `bounced`, `deferred`, `rejected` | | `queue_id` | string | Filter by specific queue ID | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** ```json { "total": 500, "page": 1, "limit": 50, "pages": 10, "data": [ { "id": 12345, "time": "2025-12-25T10:30:00Z", "program": "postfix/smtp", "priority": "info", "message": "ABC123DEF: to=...", "queue_id": "ABC123DEF", "message_id": "", "sender": "sender@example.com", "recipient": "user@example.com", "status": "sent", "relay": "mail.example.com[1.2.3.4]:25", "delay": 1.5, "dsn": "2.0.0", "correlation_key": "abc123..." } ] } ``` --- #### GET /logs/postfix/by-queue/{queue_id} Get all Postfix logs for a specific Queue-ID with linked Rspamd data. **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `queue_id` | string | The Postfix queue ID | **Response:** ```json { "queue_id": "ABC123DEF", "correlation_key": "abc123...", "rspamd": { "score": 0.5, "required_score": 15, "action": "no action", "symbols": {...}, "is_spam": false, "direction": "outbound", "subject": "Hello World" }, "logs": [ { "id": 12345, "time": "2025-12-25T10:30:00Z", "program": "postfix/smtpd", "priority": "info", "message": "ABC123DEF: client=...", "queue_id": "ABC123DEF", "message_id": "", "sender": "sender@example.com", "recipient": "user@example.com", "status": null, "relay": null, "delay": null, "dsn": null } ] } ``` --- ### Rspamd Logs #### GET /logs/rspamd Get Rspamd spam analysis logs. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `page` | int | Page number (default: 1) | | `limit` | int | Items per page (default: 50, max: 500) | | `search` | string | Search in subject, sender, message_id | | `sender` | string | Filter by sender | | `direction` | string | Filter: `inbound`, `outbound`, `internal`, `unknown` | | `min_score` | float | Minimum spam score | | `max_score` | float | Maximum spam score | | `action` | string | Filter by action: `no action`, `greylist`, `add header`, `reject` | | `is_spam` | boolean | Filter spam only (`true`) or clean only (`false`) | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** ```json { "total": 1000, "page": 1, "limit": 50, "pages": 20, "data": [ { "id": 5678, "time": "2025-12-25T10:30:00Z", "message_id": "", "subject": "Hello World", "size": 1024, "sender_smtp": "sender@example.com", "recipients_smtp": ["user@example.com"], "score": 0.5, "required_score": 15, "action": "no action", "direction": "outbound", "ip": "192.168.1.100", "is_spam": false, "has_auth": true, "user": "sender@example.com", "symbols": { "MAILCOW_AUTH": {"score": -20, "description": "mailcow authenticated"}, "RCVD_COUNT_ZERO": {"score": 0, "options": ["0"]} }, "correlation_key": "abc123..." } ] } ``` --- ### Netfilter Logs #### GET /logs/netfilter Get Netfilter authentication failure logs. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `page` | int | Page number (default: 1) | | `limit` | int | Items per page (default: 50, max: 500) | | `search` | string | Search in message, IP, username | | `ip` | string | Filter by IP address | | `username` | string | Filter by username | | `action` | string | Filter: `warning`, `banned` | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** ```json { "total": 100, "page": 1, "limit": 50, "pages": 2, "data": [ { "id": 999, "time": "2025-12-25T10:30:00Z", "priority": "warn", "message": "1.1.1.1 matched rule id 3...", "ip": "1.1.1.1", "rule_id": 3, "attempts_left": 9, "username": "user@example.com", "auth_method": "SASL LOGIN", "action": "warning" } ] } ``` --- ## Queue & Quarantine ### GET /queue Get current mail queue from Mailcow (real-time). **Response:** ```json { "total": 5, "data": [ { "queue_name": "deferred", "queue_id": "ABC123DEF", "arrival_time": 1735123456, "message_size": 515749, "forced_expire": false, "sender": "sender@example.com", "recipients": [ "user@example.com (connect to example.com[1.2.3.4]:25: Connection timed out)" ] } ] } ``` --- ### GET /quarantine Get quarantined messages from Mailcow (real-time). **Response:** ```json { "total": 3, "data": [ { "id": 123, "subject": "Suspicious Email", "sender": "spammer@evil.com", "recipients": ["user@example.com"], "created": "2025-12-25T10:30:00Z", "reason": "High spam score" } ] } ``` --- ## Statistics ### GET /stats/dashboard Get main dashboard statistics. **Response:** ```json { "messages": { "24h": 1234, "7d": 8765, "30d": 34567 }, "spam": { "24h": 56, "7d": 234, "percentage_24h": 4.54 }, "failed_deliveries": { "24h": 12, "7d": 45 }, "auth_failures": { "24h": 89, "7d": 456 }, "direction": { "inbound_24h": 800, "outbound_24h": 434, "internal_24h": 120 } } ``` --- ### GET /stats/timeline Get message timeline for charts. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `hours` | int | Number of hours to show (default: 24) | **Response:** ```json { "timeline": [ { "hour": "2025-12-25T08:00:00Z", "total": 45, "spam": 2, "clean": 43 }, { "hour": "2025-12-25T09:00:00Z", "total": 67, "spam": 5, "clean": 62 } ] } ``` --- ### GET /stats/top-spam-triggers Get top spam detection symbols. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Number of results (default: 10) | **Response:** ```json { "triggers": [ {"symbol": "RCVD_IN_DNSWL_NONE", "count": 456}, {"symbol": "DKIM_SIGNED", "count": 234}, {"symbol": "SPF_PASS", "count": 200} ] } ``` --- ### GET /stats/top-blocked-ips Get top blocked/warned IP addresses. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Number of results (default: 10) | **Response:** ```json { "blocked_ips": [ { "ip": "1.1.1.1", "count": 45, "last_seen": "2025-12-25T10:30:00Z" } ] } ``` --- ### GET /stats/recent-activity Get recent message activity stream. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `limit` | int | Number of results (default: 20) | **Response:** ```json { "activity": [ { "time": "2025-12-25T10:30:00Z", "sender": "user@example.com", "recipient": "other@gmail.com", "subject": "Hello World", "direction": "outbound", "status": "delivered", "correlation_key": "abc123..." } ] } ``` --- ## Status ### GET /status/containers Get status of all Mailcow containers. **Response:** ```json { "containers": { "postfix-mailcow": { "name": "postfix", "state": "running", "started_at": "2025-12-20T08:00:00Z" }, "dovecot-mailcow": { "name": "dovecot", "state": "running", "started_at": "2025-12-20T08:00:00Z" } }, "summary": { "running": 18, "stopped": 0, "total": 18 } } ``` --- ### GET /status/storage Get storage/disk usage information. **Response:** ```json { "disk": "/dev/sda1", "used": "45G", "total": "100G", "used_percent": "45%" } ``` --- ### GET /status/version Get Mailcow version and update status. **Response:** ```json { "current_version": "2025-01", "latest_version": "2025-01a", "update_available": true, "changelog": "Bug fixes and improvements...", "last_checked": "2025-12-25T10:30:00Z" } ``` --- ### GET /status/app-version Get application version and check for updates from GitHub. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `force` | boolean | Force a fresh version check regardless of cache age (default: false) | **Response:** ```json { "current_version": "1.4.9", "latest_version": "1.4.9", "update_available": false, "changelog": "### Added\n\n#### Background Jobs Enhanced UI\n- Compact layout...", "last_checked": "2026-01-08T15:52:46Z" } ``` **Implementation Notes:** - Version checks are performed by the scheduler every 6 hours - Results are cached in `app_version_cache` (managed by `scheduler.py`) - Status endpoint retrieves cached data via `get_app_version_cache()` - Use `force=true` parameter to bypass cache and trigger immediate check - All timestamps include UTC timezone indicator ('Z' suffix) - Changelog is retrieved from GitHub releases in Markdown format **Version Check Process:** 1. Scheduler job `check_app_version_update` runs every 6 hours 2. Fetches latest release from `https://api.github.com/repos/ShlomiPorush/mailcow-logs-viewer/releases/latest` 3. Compares current version (from `/app/VERSION` file) with latest GitHub release 4. Updates cache with result and changelog 5. Job status tracked with `update_job_status()` (visible in Settings > Background Jobs) --- ### GET /status/app-version/changelog/{version} Get changelog for a specific app version from GitHub. **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `version` | string | Version number (with or without 'v' prefix, e.g., "1.4.6" or "v1.4.6") | **Response:** ```json { "version": "1.4.6", "changelog": "Full changelog in Markdown format for the specified version..." } ``` **Note:** Returns the changelog from the GitHub release for the specified version tag. --- ### GET /status/mailcow-connection Check Mailcow API connection status. **Response:** ```json { "connected": true, "timestamp": "2026-01-05T15:52:46Z" } ``` **Note:** Returns connection status and current timestamp in UTC format. --- ### GET /status/mailcow-info Get Mailcow system information. **Response:** ```json { "domains": { "total": 5, "active": 5 }, "mailboxes": { "total": 25, "active": 23 }, "aliases": { "total": 50, "active": 48 } } ``` --- ### GET /status/summary Get combined status summary for dashboard. **Response:** ```json { "containers": { "running": 18, "stopped": 0, "total": 18 }, "storage": { "used_percent": "45%", "used": "45G", "total": "100G" }, "system": { "domains": 5, "mailboxes": 25, "aliases": 50 } } ``` --- ## Settings ### GET /settings/info Get system configuration and status information. **Response:** ```json { "configuration": { "mailcow_url": "https://mail.example.com", "local_domains": ["example.com"], "fetch_interval": 60, "fetch_count_postfix": 2000, "fetch_count_rspamd": 500, "fetch_count_netfilter": 500, "retention_days": 7, "timezone": "UTC", "app_title": "Mailcow Logs Viewer", "log_level": "WARNING", "blacklist_enabled": true, "blacklist_count": 3, "max_search_results": 1000, "csv_export_limit": 10000, "scheduler_workers": 4, "auth_enabled": false, "auth_username": null, "maxmind_status": { "configured": true, "valid": true, "error": null } }, "import_status": { "postfix": { "last_import": "2025-12-25T10:30:00Z", "last_fetch_run": "2025-12-25T10:35:00Z", "total_entries": 50000, "oldest_entry": "2025-12-18T00:00:00Z" }, "rspamd": { "last_import": "2025-12-25T10:30:00Z", "last_fetch_run": "2025-12-25T10:35:00Z", "total_entries": 45000, "oldest_entry": "2025-12-18T00:00:00Z" }, "netfilter": { "last_import": "2025-12-25T10:30:00Z", "last_fetch_run": "2025-12-25T10:35:00Z", "total_entries": 1000, "oldest_entry": "2025-12-18T00:00:00Z" } }, "correlation_status": { "last_update": "2025-12-25T10:30:00Z", "total": 40000, "complete": 39500, "incomplete": 500, "expired": 100, "completion_rate": 98.75 }, "background_jobs": { "fetch_logs": { "interval": "60 seconds", "description": "Imports logs from Mailcow API", "status": "success", "last_run": "2026-01-08T12:14:56Z", "error": null }, "complete_correlations": { "interval": "120 seconds (2 minutes)", "description": "Links Postfix logs to messages", "status": "running", "last_run": "2026-01-08T12:13:56Z", "error": null, "pending_items": 93 }, "update_final_status": { "interval": "120 seconds (2 minutes)", "description": "Updates final status for correlations with late-arriving Postfix logs", "max_age": "10 minutes", "status": "success", "last_run": "2026-01-08T12:13:56Z", "error": null, "pending_items": 25 }, "expire_correlations": { "interval": "60 seconds (1 minute)", "description": "Marks old incomplete correlations as expired", "expire_after": "10 minutes", "status": "success", "last_run": "2026-01-08T12:14:45Z", "error": null }, "cleanup_logs": { "schedule": "Daily at 2 AM", "description": "Removes old logs based on retention period", "retention": "7 days", "status": "scheduled", "last_run": "2026-01-08T02:00:00Z", "error": null }, "check_app_version": { "interval": "6 hours", "description": "Checks for application updates from GitHub", "status": "success", "last_run": "2026-01-08T10:00:00Z", "error": null }, "dns_check": { "interval": "6 hours", "description": "Validates DNS records (SPF, DKIM, DMARC) for all active domains", "status": "success", "last_run": "2026-01-08T08:00:00Z", "error": null }, "update_geoip": { "schedule": "Weekly (Sunday 3 AM)", "description": "Updates MaxMind GeoIP databases for DMARC source IP enrichment", "status": "success", "last_run": "2026-01-05T03:00:00Z", "error": null } }, "recent_incomplete_correlations": [ { "message_id": "", "queue_id": "ABC123", "sender": "user@example.com", "recipient": "other@gmail.com", "created_at": "2025-12-25T10:28:00Z", "age_minutes": 2 } ] } ``` **Background Jobs Status Tracking:** Each background job reports real-time execution status: | Field | Type | Description | |-------|------|-------------| | `interval` / `schedule` | string | How often the job runs | | `description` | string | Human-readable job description | | `status` | string | Current status: `running`, `success`, `failed`, `idle`, `scheduled` | | `last_run` | datetime | UTC timestamp of last execution (with 'Z' suffix) | | `error` | string / null | Error message if job failed, otherwise null | | `pending_items` | int | Number of items waiting (for correlation jobs only) | | `max_age` / `expire_after` / `retention` | string | Job-specific configuration | **Status Values:** - `running` - Job is currently executing - `success` - Job completed successfully - `failed` - Job encountered an error - `idle` - Job hasn't run yet - `scheduled` - Job is scheduled but runs infrequently (e.g., daily cleanup) **Job Descriptions:** 1. **fetch_logs**: Fetches Postfix, Rspamd, and Netfilter logs from Mailcow API every 60 seconds 2. **complete_correlations**: Links Postfix logs to message correlations every 2 minutes 3. **update_final_status**: Updates message delivery status when late-arriving Postfix logs are found 4. **expire_correlations**: Marks old incomplete correlations as expired after 10 minutes 5. **cleanup_logs**: Removes logs older than retention period (runs daily at 2 AM) 6. **check_app_version**: Checks GitHub for application updates every 6 hours 7. **dns_check**: Validates DNS records (SPF, DKIM, DMARC) for all active domains every 6 hours 8. **update_geoip**: Updates MaxMind GeoLite2 databases (City + ASN) for DMARC source IP enrichment (runs weekly on Sunday at 3 AM) --- ### GET /settings/health Detailed health check with timing information. **Response:** ```json { "status": "healthy", "timestamp": "2025-12-25T10:30:00Z", "database": { "status": "connected", "response_time_ms": 1.25 }, "recent_activity": { "last_5_minutes": { "postfix_imported": 45, "rspamd_imported": 42, "correlations_created": 40 } } } ``` --- ## SMTP & IMAP Test ### POST /api/settings/test/smtp Test SMTP connection with detailed logging for diagnostics. **Request:** No body required **Response:** ```json { "success": true, "logs": [ "Starting SMTP connection test...", "Host: mail.example.com", "Port: 587", "Use TLS: true", "User: noreply@example.com", "Connecting to SMTP server...", "Connected", "Starting TLS...", "TLS established", "Logging in...", "Login successful", "Sending test email...", "Test email sent successfully", "Connection closed", "✓ SMTP test completed successfully" ] } ``` **Error Response:** ```json { "success": false, "logs": [ "Starting SMTP connection test...", "Host: mail.example.com", "Port: 587", "Connecting to SMTP server...", "✗ Authentication failed: (535, b'5.7.8 Error: authentication failed')" ] } ``` **Response Fields:** - `success`: Boolean indicating if test passed - `logs`: Array of log messages showing connection attempt details **Notes:** - Sends actual test email to configured admin email address - Tests full connection flow: connect → TLS → authenticate → send - Useful for diagnosing SMTP configuration issues - Returns detailed error messages on failure --- ### POST /api/settings/test/imap Test IMAP connection with detailed logging for diagnostics. **Request:** No body required **Response:** ```json { "success": true, "logs": [ "Starting IMAP connection test...", "Host: mail.example.com", "Port: 993", "Use SSL: true", "User: dmarc@example.com", "Folder: INBOX", "Connecting to IMAP server...", "Connected using SSL", "Logging in...", "Login successful", "Listing mailboxes...", "Found 5 mailboxes:", " - \"INBOX\"", " - \"Sent\"", " - \"Drafts\"", " - \"Spam\"", " - \"Trash\"", "Selecting folder: INBOX", "Folder selected: 42 messages", "Searching for emails...", "Found 42 emails in folder", "Connection closed", "✓ IMAP test completed successfully" ] } ``` **Error Response:** ```json { "success": false, "logs": [ "Starting IMAP connection test...", "Host: mail.example.com", "Port: 993", "Connecting to IMAP server...", "✗ IMAP error: [AUTHENTICATIONFAILED] Authentication failed." ] } ``` **Response Fields:** - `success`: Boolean indicating if test passed - `logs`: Array of log messages showing connection attempt details **Notes:** - Tests full connection flow: connect → authenticate → list folders → select folder - Shows available mailboxes and message count - Useful for diagnosing IMAP configuration issues - Does not modify or process any emails - Returns detailed error messages on failure --- ## Export ### GET /export/postfix/csv Export Postfix logs to CSV file. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `search` | string | Search filter | | `sender` | string | Filter by sender | | `recipient` | string | Filter by recipient | | `status` | string | Filter by status | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** CSV file download **Columns:** Time, Program, Priority, Queue ID, Message ID, Sender, Recipient, Status, Relay, Delay, DSN, Message --- ### GET /export/rspamd/csv Export Rspamd logs to CSV file. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `search` | string | Search filter | | `sender` | string | Filter by sender | | `direction` | string | Filter by direction | | `min_score` | float | Minimum spam score | | `max_score` | float | Maximum spam score | | `is_spam` | boolean | Filter by spam status | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** CSV file download **Columns:** Time, Message ID, Subject, Sender, Recipients, Score, Required Score, Action, Direction, Is Spam, Has Auth, User, IP, Size, Top Symbols --- ### GET /export/netfilter/csv Export Netfilter logs to CSV file. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `search` | string | Search filter | | `ip` | string | Filter by IP | | `username` | string | Filter by username | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** CSV file download **Columns:** Time, IP, Username, Auth Method, Action, Attempts Left, Rule ID, Priority, Message --- ### GET /export/messages/csv Export Messages (correlations) to CSV file. **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `search` | string | Search filter | | `sender` | string | Filter by sender | | `recipient` | string | Filter by recipient | | `direction` | string | Filter by direction | | `status` | string | Filter by status | | `user` | string | Filter by authenticated user | | `ip` | string | Filter by IP address | | `start_date` | datetime | Start date | | `end_date` | datetime | End date | **Response:** CSV file download **Columns:** Time, Sender, Recipient, Subject, Direction, Status, Queue ID, Message ID, Spam Score, Is Spam, User, IP, Is Complete --- ## DMARC ### Overview The DMARC module provides comprehensive email authentication monitoring through DMARC (Domain-based Message Authentication, Reporting & Conformance) aggregate reports. It includes automatic report parsing, GeoIP enrichment for source IPs, and detailed analytics. **Features:** - Automatic DMARC report parsing (XML, GZ, ZIP formats) - GeoIP enrichment (country, city, ISP/ASN) via MaxMind databases - Domain-centric view with daily aggregation - Source IP analysis with authentication results - Historical trending and compliance monitoring --- ### GET /api/dmarc/domains Get list of all domains with DMARC statistics. **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `days` | integer | 30 | Number of days to look back (1-365) | **Response:** ```json { "total_domains": 5, "total_messages": 12458, "total_unique_ips": 142, "overall_dmarc_pass_pct": 97.2, "domains": [ { "domain": "example.com", "total_messages": 8234, "unique_ips": 89, "dmarc_pass_pct": 98.5, "spf_pass_pct": 99.1, "dkim_pass_pct": 98.9, "policy_p": "reject", "policy_sp": null, "last_report_date": 1704758400 } ] } ``` **Response Fields:** - `total_domains`: Number of domains with DMARC reports - `total_messages`: Total email messages across all domains - `total_unique_ips`: Total unique source IPs - `overall_dmarc_pass_pct`: Overall DMARC pass rate percentage - `domains`: Array of domain statistics **Domain Object Fields:** - `domain`: Domain name - `total_messages`: Total messages for this domain - `unique_ips`: Number of unique source IPs - `dmarc_pass_pct`: Percentage of messages passing both SPF and DKIM - `spf_pass_pct`: SPF pass rate - `dkim_pass_pct`: DKIM pass rate - `policy_p`: Published DMARC policy (none, quarantine, reject) - `policy_sp`: Subdomain policy (if different from main policy) - `last_report_date`: Unix timestamp of most recent report --- ### GET /api/dmarc/domains/{domain}/overview Get detailed overview for a specific domain with daily breakdown. **Path Parameters:** - `domain`: Domain name (URL encoded) **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `days` | integer | 30 | Number of days to look back (1-365) | **Response:** ```json { "domain": "example.com", "total_messages": 8234, "unique_ips": 89, "unique_reporters": 12, "dmarc_pass_pct": 98.5, "spf_pass_pct": 99.1, "dkim_pass_pct": 98.9, "policy": { "p": "reject", "sp": null, "adkim": "r", "aspf": "r", "pct": 100, "fo": "0" }, "daily_stats": [ { "date": 1704758400, "total_messages": 287, "dmarc_pass_pct": 98.3, "spf_pass_pct": 99.0, "dkim_pass_pct": 98.6 } ] } ``` **Response Fields:** - `domain`: Domain name - `total_messages`: Total messages in period - `unique_ips`: Number of unique source IPs - `unique_reporters`: Number of unique organizations sending reports - `dmarc_pass_pct`: DMARC pass rate (SPF + DKIM aligned) - `spf_pass_pct`: SPF pass rate - `dkim_pass_pct`: DKIM pass rate - `policy`: Published DMARC policy object - `daily_stats`: Array of daily statistics **Policy Object:** - `p`: Domain policy (none, quarantine, reject) - `sp`: Subdomain policy - `adkim`: DKIM alignment mode (r=relaxed, s=strict) - `aspf`: SPF alignment mode (r=relaxed, s=strict) - `pct`: Percentage of messages to apply policy to - `fo`: Failure reporting options **Daily Stats Object:** - `date`: Unix timestamp (midnight UTC) - `total_messages`: Messages for this day - `dmarc_pass_pct`: DMARC pass rate - `spf_pass_pct`: SPF pass rate - `dkim_pass_pct`: DKIM pass rate --- ### GET /api/dmarc/domains/{domain}/reports Get daily aggregated reports for a specific domain. **Path Parameters:** - `domain`: Domain name (URL encoded) **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `days` | integer | 30 | Number of days to look back (1-365) | **Response:** ```json { "domain": "example.com", "reports": [ { "date": 1704758400, "report_count": 12, "unique_ips": 45, "total_messages": 287, "dmarc_pass_pct": 98.3, "spf_pass_pct": 99.0, "dkim_pass_pct": 98.6, "reporters": [ "Google", "Microsoft", "Yahoo" ] } ] } ``` **Response Fields:** - `domain`: Domain name - `reports`: Array of daily aggregated reports **Report Object:** - `date`: Unix timestamp (midnight UTC) - `report_count`: Number of DMARC reports received for this day - `unique_ips`: Number of unique source IPs - `total_messages`: Total messages in all reports - `dmarc_pass_pct`: DMARC compliance rate - `spf_pass_pct`: SPF pass rate - `dkim_pass_pct`: DKIM pass rate - `reporters`: Array of organizations that sent reports (e.g., "Google", "Microsoft") --- ### GET /api/dmarc/domains/{domain}/sources Get source IP analysis with GeoIP enrichment. **Path Parameters:** - `domain`: Domain name (URL encoded) **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `days` | integer | 30 | Number of days to look back (1-365) | **Response:** ```json { "domain": "example.com", "sources": [ { "source_ip": "8.8.8.8", "total_messages": 1250, "dmarc_pass_pct": 100.0, "spf_pass_pct": 100.0, "dkim_pass_pct": 100.0, "country_code": "US", "country_name": "United States", "country_emoji": "🇺🇸", "city": "Mountain View", "asn": "AS15169", "asn_org": "Google LLC", "first_seen": 1704153600, "last_seen": 1704758400 }, { "source_ip": "212.199.162.78", "total_messages": 456, "dmarc_pass_pct": 98.2, "spf_pass_pct": 99.1, "dkim_pass_pct": 98.5, "country_code": "IL", "country_name": "Israel", "country_emoji": "🇮🇱", "city": "Tel Aviv", "asn": "AS8551", "asn_org": "Bezeq International Ltd.", "first_seen": 1704240000, "last_seen": 1704758400 } ] } ``` **Response Fields:** - `domain`: Domain name - `sources`: Array of source IP objects (ordered by message count, descending) **Source Object Fields:** - `source_ip`: IP address of sending server - `total_messages`: Number of messages from this IP - `dmarc_pass_pct`: DMARC pass rate for this IP - `spf_pass_pct`: SPF pass rate - `dkim_pass_pct`: DKIM pass rate - `country_code`: ISO 3166-1 alpha-2 country code (e.g., "US", "IL") - `country_name`: Full country name - `country_emoji`: Flag emoji representation (e.g., 🇺🇸, 🇮🇱) - `city`: City name (from MaxMind City database) - `asn`: Autonomous System Number (e.g., "AS15169") - `asn_org`: ISP/Organization name from ASN database - `first_seen`: Unix timestamp of first message from this IP - `last_seen`: Unix timestamp of last message from this IP **GeoIP Notes:** - GeoIP fields may be `null` if MaxMind databases are not configured - `country_emoji` defaults to 🌍 (globe) when country is unknown - GeoIP data requires MaxMind GeoLite2 databases (City + ASN) - City accuracy varies by IP (typically accurate to city level for data center IPs) - ASN provides ISP/hosting provider information --- ### POST /api/dmarc/upload Upload and parse a DMARC aggregate report file. **Content-Type:** `multipart/form-data` **Form Data:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | DMARC report file (XML, GZ, or ZIP format) | **Supported File Formats:** - `.xml` - Raw XML DMARC report - `.gz` - Gzip-compressed XML report (most common) - `.zip` - ZIP-compressed XML report (used by Google) **Request Example:** ```bash curl -X POST http://your-server:8080/api/dmarc/upload \ -u username:password \ -F "file=@google.com!example.com!1704067200!1704153599.xml.gz" ``` **Success Response (201 Created):** ```json { "status": "success", "message": "Uploaded report for example.com from Google", "report_id": 123, "records_count": 45 } ``` **Duplicate Response (200 OK):** ```json { "status": "duplicate", "message": "Report 12345678901234567890 already exists" } ``` **Error Response (400 Bad Request):** ```json { "detail": "Failed to parse DMARC report" } ``` **Error Response (500 Internal Server Error):** ```json { "detail": "Error message with details" } ``` **Response Fields:** **Success Response:** - `status`: "success" - `message`: Human-readable description of uploaded report - `report_id`: Database ID of created report - `records_count`: Number of source IP records parsed **Duplicate Response:** - `status`: "duplicate" - `message`: Indicates report already exists (based on unique report_id from XML) **Processing Details:** 1. File is decompressed (if GZ or ZIP) 2. XML is parsed and validated 3. Report metadata extracted (domain, org, date range, policy) 4. Individual records parsed (source IP, counts, auth results) 5. GeoIP enrichment applied to each source IP (if MaxMind configured) 6. Data stored in database with proper indexing 7. Duplicate detection based on unique report_id from XML **Parsed Data Includes:** - Report metadata (report ID, organization, date range) - Domain and published DMARC policy - Individual source records: - Source IP address - Message count from this source - SPF/DKIM authentication results - Policy evaluation (disposition) - GeoIP enrichment (country, city, ISP/ASN) **GeoIP Enrichment:** - Automatically applied to all source IPs during upload - Uses MaxMind GeoLite2 databases (if configured) - Gracefully degrades if databases unavailable - Enriches with: country, city, ISP, ASN **File Naming Convention:** DMARC report filenames typically follow this pattern: ``` !!!. ``` Example: `google.com!example.com!1704067200!1704153599.xml.gz` **Notes:** - Reports are identified by unique report_id (from XML) - Duplicate uploads are detected and rejected gracefully - Large reports (1000+ records) may take a few seconds to process - File size limit depends on server configuration (typically 10MB) - Malformed XML files are rejected with 400 error --- ## DMARC IMAP Auto-Import The DMARC module supports automatic import of DMARC reports via IMAP. This allows the system to periodically check a mailbox and automatically process incoming reports without manual uploads. **Features:** - Automatic periodic syncing from IMAP mailbox - Configurable sync interval and folder - Manual sync trigger via API - Comprehensive sync history tracking - Email notifications on sync failures - Support for SSL/TLS connections - Automatic duplicate detection **Configuration:** Set these environment variables to enable IMAP auto-import: - `DMARC_IMAP_ENABLED=true` - `DMARC_IMAP_HOST=mail.example.com` - `DMARC_IMAP_PORT=993` - `DMARC_IMAP_USE_SSL=true` - `DMARC_IMAP_USER=dmarc@example.com` - `DMARC_IMAP_PASSWORD=your-password` - `DMARC_IMAP_FOLDER=INBOX` - `DMARC_IMAP_INTERVAL=3600` (seconds between syncs) - `DMARC_IMAP_DELETE_AFTER=false` (delete processed emails) - `DMARC_MANUAL_UPLOAD_ENABLED=true` (allow manual uploads) --- ### GET /api/dmarc/imap/status Get current IMAP auto-import configuration and last sync information. **Response:** ```json { "enabled": true, "manual_upload_enabled": true, "host": "mail.example.com", "port": 993, "use_ssl": true, "user": "dmarc@example.com", "folder": "INBOX", "interval_seconds": 3600, "delete_after_processing": false, "last_sync": { "sync_id": 42, "sync_type": "auto", "status": "success", "started_at": "2026-01-12T08:45:20Z", "completed_at": "2026-01-12T08:45:21Z", "emails_found": 5, "emails_processed": 5, "reports_created": 4, "reports_duplicate": 0, "reports_failed": 1, "error_message": null } } ``` **Response Fields:** - `enabled`: Whether IMAP auto-import is enabled - `manual_upload_enabled`: Whether manual uploads are still allowed - `host`: IMAP server hostname - `port`: IMAP server port (typically 993 for SSL, 143 for non-SSL) - `use_ssl`: Whether SSL/TLS is used - `user`: IMAP username/email - `folder`: Mailbox folder being monitored (e.g., "INBOX") - `interval_seconds`: Seconds between automatic sync runs - `delete_after_processing`: Whether emails are deleted after successful processing - `last_sync`: Last sync operation details (null if never run) **Last Sync Object:** - `sync_id`: Unique sync operation ID - `sync_type`: "auto" (scheduled) or "manual" (API triggered) - `status`: "success", "error", or "running" - `started_at`: ISO 8601 timestamp with Z suffix - `completed_at`: ISO 8601 timestamp with Z suffix (null if running) - `emails_found`: Number of DMARC emails found in folder - `emails_processed`: Number of emails successfully processed - `reports_created`: Number of new DMARC reports created - `reports_duplicate`: Number of duplicate reports skipped - `reports_failed`: Number of emails that failed processing - `error_message`: Error description if sync failed **Notes:** - Sensitive information (password) is never returned - Returns 404 if IMAP auto-import is not configured - Last sync information persists across restarts --- ### POST /api/dmarc/imap/sync Manually trigger IMAP sync operation. **Request:** No body required **Response:** ```json { "sync_id": 43, "sync_type": "manual", "status": "success", "started_at": "2026-01-12T10:30:00Z", "completed_at": "2026-01-12T10:30:05Z", "emails_found": 3, "emails_processed": 3, "reports_created": 2, "reports_duplicate": 1, "reports_failed": 0, "error_message": null, "failed_emails": null } ``` **Error Response (IMAP disabled):** ```json { "status": "disabled", "message": "DMARC IMAP sync is not enabled" } ``` **Error Response (Connection failed):** ```json { "sync_id": 44, "sync_type": "manual", "status": "error", "started_at": "2026-01-12T10:35:00Z", "completed_at": "2026-01-12T10:35:30Z", "emails_found": 0, "emails_processed": 0, "reports_created": 0, "reports_duplicate": 0, "reports_failed": 0, "error_message": "[Errno 110] Connection timed out", "failed_emails": null } ``` **Response Fields:** - `sync_id`: Unique ID for this sync operation - `sync_type`: Always "manual" for API-triggered syncs - `status`: "success" or "error" - `started_at`: ISO 8601 timestamp when sync started - `completed_at`: ISO 8601 timestamp when sync finished - `emails_found`: Number of DMARC emails found - `emails_processed`: Number of emails processed - `reports_created`: Number of new reports created - `reports_duplicate`: Number of duplicate reports skipped - `reports_failed`: Number of emails that failed processing - `error_message`: Error description if sync failed (null on success) - `failed_emails`: Array of failed email details (null if none failed) **Failed Email Object** (when reports_failed > 0): ```json { "email_id": "21", "message_id": "", "subject": "Report Domain: example.com", "error": "Not a valid DMARC report email" } ``` **Notes:** - Returns immediately with sync results (synchronous operation) - Can be called while automatic sync is disabled - Creates sync history record for tracking - Duplicate reports are detected and skipped gracefully - Failed emails are logged but don't prevent other emails from processing - Email notifications sent if SMTP configured and failures occur --- ### GET /api/dmarc/imap/history Get history of IMAP sync operations. **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | integer | 20 | Maximum number of sync records to return (1-100) | **Response:** ```json { "data": [ { "id": 43, "sync_type": "manual", "status": "success", "started_at": "2026-01-12T10:30:00Z", "completed_at": "2026-01-12T10:30:05Z", "duration_seconds": 5, "emails_found": 3, "emails_processed": 3, "reports_created": 2, "reports_duplicate": 1, "reports_failed": 0, "error_message": null, "failed_emails": null }, { "id": 42, "sync_type": "auto", "status": "success", "started_at": "2026-01-12T08:45:20Z", "completed_at": "2026-01-12T08:45:21Z", "duration_seconds": 1, "emails_found": 5, "emails_processed": 5, "reports_created": 4, "reports_duplicate": 0, "reports_failed": 1, "error_message": "1 emails failed to process", "failed_emails": [ { "email_id": "21", "message_id": "", "subject": "FW: Report", "error": "No DMARC attachments found" } ] } ] } ``` **Response Fields:** - `data`: Array of sync history records (newest first) **Sync Record Fields:** - `id`: Unique sync ID - `sync_type`: "auto" or "manual" - `status`: "success", "error", or "running" - `started_at`: ISO 8601 timestamp - `completed_at`: ISO 8601 timestamp (null if still running) - `duration_seconds`: Sync duration in seconds (null if still running) - `emails_found`: Number of emails found - `emails_processed`: Number of emails processed - `reports_created`: Number of new reports created - `reports_duplicate`: Number of duplicates skipped - `reports_failed`: Number of failed emails - `error_message`: Error description (null if no errors) - `failed_emails`: Array of failed email details (null if none) **Notes:** - Results ordered by most recent first - Running syncs show null for completed_at and duration_seconds - Failed email details include message ID, subject, and error reason - Useful for debugging sync issues and monitoring system health - History persists across application restarts --- ## TLS-RPT (TLS Reporting) ### Overview TLS-RPT (TLS Reporting) provides visibility into TLS connection failures when other mail servers attempt to deliver emails to your domain. This helps identify MTA-STS policy issues and certificate problems. --- ### GET /api/dmarc/domains/{domain}/tls-reports Get TLS reports for a specific domain (individual reports). **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `domain` | string | Domain name | **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `days` | integer | 30 | Number of days to look back | **Response:** ```json { "domain": "example.com", "total": 15, "data": [ { "id": 1, "report_id": "2026-01-14T00:00:00Z!example.com!google.com", "organization_name": "Google Inc.", "start_datetime": "2026-01-14T00:00:00Z", "end_datetime": "2026-01-15T00:00:00Z", "total_success": 1250, "total_fail": 5, "success_rate": 99.6 } ] } ``` --- ### GET /api/dmarc/domains/{domain}/tls-reports/daily Get TLS reports aggregated by date (daily view). **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `domain` | string | Domain name | **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `days` | integer | 30 | Number of days to look back | | `page` | integer | 1 | Page number | | `page_size` | integer | 20 | Items per page | **Response:** ```json { "domain": "example.com", "totals": { "total_days": 14, "total_reports": 28, "total_successful_sessions": 15000, "total_failed_sessions": 25, "overall_success_rate": 99.83 }, "data": [ { "date": "2026-01-17", "report_count": 3, "organization_count": 2, "organizations": ["Google Inc.", "Microsoft Corporation"], "total_success": 1500, "total_fail": 2, "success_rate": 99.87 } ] } ``` --- ### GET /api/dmarc/domains/{domain}/tls-reports/{report_date}/details Get detailed TLS reports for a specific date. **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `domain` | string | Domain name | | `report_date` | string | Date in YYYY-MM-DD format | **Response:** ```json { "domain": "example.com", "date": "2026-01-17", "stats": { "total_reports": 3, "total_providers": 2, "total_success": 1500, "total_fail": 2, "total_sessions": 1502, "success_rate": 99.87 }, "providers": [ { "report_id": "2026-01-17T00:00:00Z!example.com!google.com", "organization_name": "Google Inc.", "contact_info": "smtp-tls-reporting@google.com", "start_datetime": "2026-01-17T00:00:00Z", "end_datetime": "2026-01-18T00:00:00Z", "successful_sessions": 1200, "failed_sessions": 1, "total_sessions": 1201, "success_rate": 99.92, "policies": [ { "policy_type": "sts", "policy_domain": "example.com", "mx_host": "mail.example.com", "successful_sessions": 1200, "failed_sessions": 1, "total_sessions": 1201, "success_rate": 99.92, "failure_details": null } ] } ] } ``` --- ### POST /api/dmarc/upload (TLS-RPT Support) The existing DMARC upload endpoint also accepts TLS-RPT reports. **Supported TLS-RPT Formats:** - `.json.gz` - Gzip-compressed JSON (standard format) - `.json` - Plain JSON **Detection:** - File is identified as TLS-RPT if JSON contains `"policies"` array - TLS-RPT reports use RFC 8460 JSON format --- ## Error Responses All endpoints may return the following error responses: ### 400 Bad Request ```json { "detail": "Invalid parameter value" } ``` ### 404 Not Found ```json { "detail": "Resource not found" } ``` ### 401 Unauthorized ```json { "detail": "Authentication required" } ``` **Note:** Returned when authentication is enabled but no valid credentials are provided. The response does not include `WWW-Authenticate` header to prevent browser popup dialogs. ### 500 Internal Server Error ```json { "error": "Internal server error", "detail": "Error description (only in debug mode)" } ```