Modern REST API for managing Dovecot/Postfix mail server with LDA routing and Fetchmail integration
A minimalistic FastAPI based administration interface for complete mail server management. Provides secure endpoints for user management, routing configuration, and centralized log viewing across Dovecot, Postfix, and Fetchmail services.
- Python 3.10 or higher
- Dovecot - IMAP/POP3 mail server
- Postfix - SMTP mail transfer agent
- Fetchmail - Remote mail retrieval (optional but recommended)
- systemd - For service management
- Linux (Ubuntu 20.04+, Debian 11+, or similar)
- systemctl support for service reloading
Install required mail server components:
# Install Dovecot
sudo apt install dovecot-core dovecot-imapd dovecot-pop3d
# Install Postfix
sudo apt install postfix
# Install Fetchmail (for retrieving mail from remote servers)
sudo apt install fetchmail
# Install Python development tools
sudo apt install python3-pip python3-venvgit clone https://github.com/patrikcelko/Mail-Admin.git
cd Mail-AdminCreate configuration file:
nano .envRequired environment variables:
# Authentication (REQUIRED)
MAIL_ADMIN_USER=admin
MAIL_ADMIN_PASS=secure_password_here
# Server Settings (OPTIONAL)
MAIL_ADMIN_HOST=0.0.0.0 # Bind address
MAIL_ADMIN_PORT=8000 # Listen port
MAIL_ADMIN_ALLOW_DEBUG=false # Enable debug mode
MAIL_ADMIN_HEALTH_LOGS_LIMIT=50 # Health endpoint log countLoad configuration (if needed):
source .envConfigure Fetchmail to retrieve emails from remote servers:
sudo nano /etc/fetchmailrcExample configuration:
poll <imap>.net proto imap port 993 envelope "Received":
user "index@<email_domain>" password "<password>"
mda "/usr/local/bin/lda-alias %T %F"
ssl
sslcertck
nokeep
fetchall
no rewrite
to *Set permissions:
sudo chmod 600 /etc/fetchmailrc
sudo chown fetchmail:fetchmail /etc/fetchmailrcEnable Fetchmail service:
sudo systemctl enable fetchmail
sudo systemctl start fetchmailsudo nano /usr/local/bin/lda-aliasExample lda:
#!/usr/bin/env bash
set -euo pipefail
MAP_FILE="/etc/lda-maps.conf"
TMP="$(mktemp)"
cat > "$TMP"
rcpt_orig="${1:-}"
sender="${2:-}"
normalize_addr() {
sed -E 's/^[[:space:]]*<?([^>[:space:]]+)>?[[:space:]]*$/\1/i'
}
# --- Determination of rcpt_orig from headers ---
if [[ -z "${rcpt_orig}" || "${rcpt_orig}" == "fetchmail" || "${rcpt_orig}" != *"@"* ]]; then
if grep -qi '^Delivered-To:' "$TMP"; then
rcpt_orig="$(grep -i '^Delivered-To:' "$TMP" | head -n1 | cut -d: -f2- | normalize_addr)"
fi
fi
if [[ -z "${rcpt_orig}" || "${rcpt_orig}" == "fetchmail" || "${rcpt_orig}" != *"@"* ]]; then
if grep -qi '^X-Original-To:' "$TMP"; then
rcpt_orig="$(grep -i '^X-Original-To:' "$TMP" | head -n1 | cut -d: -f2- | normalize_addr)"
fi
fi
if [[ -z "${rcpt_orig}" || "${rcpt_orig}" == "fetchmail" || "${rcpt_orig}" != *"@"* ]]; then
if grep -qi '^Received:' "$TMP"; then
cand="$(grep -oi 'for <[^>]\+>' "$TMP" | tail -n1 | sed -E 's/^for <([^>]+)>/\1/i')"
if [[ -n "${cand}" ]]; then
rcpt_orig="$(echo "$cand" | normalize_addr)"
fi
fi
fi
if [[ -z "${rcpt_orig}" || "${rcpt_orig}" == "fetchmail" || "${rcpt_orig}" != *"@"* ]]; then
if grep -qi '^To:' "$TMP"; then
rcpt_orig="$(grep -i '^To:' "$TMP" | head -n1 | sed -E 's/^To:[[:space:]]*//i' | sed 's/,.*$//' | normalize_addr)"
fi
fi
if [[ -z "${rcpt_orig}" || "${rcpt_orig}" == "fetchmail" || "${rcpt_orig}" != *"@"* ]]; then
logger -t lda-alias "ERROR: unknown rcpt; sender=$sender"
rm -f "$TMP"
exit 67
fi
rcpt="$rcpt_orig"
# --- mapping from /etc/lda-maps.conf ---
if [[ -f "$MAP_FILE" ]]; then
while read -r pat dest rest; do
[[ -z "${pat:-}" ]] && continue
[[ "${pat:0:1}" == "#" ]] && continue
if [[ "$rcpt" == $pat ]]; then
rcpt="$dest"
break
fi
done < "$MAP_FILE"
fi
logger -t lda-alias "orig_rcpt=$rcpt_orig mapped_rcpt=$rcpt sender=${sender:-}"
ERR_TMP="$(mktemp)"
if /usr/lib/dovecot/dovecot-lda -f "${sender:-}" -a "${rcpt_orig}" -d "${rcpt}" < "$TMP" 2>"$ERR_TMP"; then
rm -f "$TMP" "$ERR_TMP"
exit 0
else
st=$?
err_msg="$(cat "$ERR_TMP" 2>/dev/null || echo 'no stderr')"
logger -t lda-alias "ERROR: dovecot-lda exit=$st for $rcpt (orig=$rcpt_orig) stderr: $err_msg"
rm -f "$TMP" "$ERR_TMP"
exit $st
fipython -m mail_adminFor production deployment with automatic startup on boot:
-
I. Create systemd service file:
sudo nano /etc/systemd/system/mail-admin.service
-
II. Add the following configuration:
[Unit] Description=Mail Admin API After=network.target dovecot.service postfix.service Wants=dovecot.service postfix.service [Service] Type=simple User=root Group=root WorkingDirectory=<working dir> # Environment variables Environment="MAIL_ADMIN_USER=admin" Environment="MAIL_ADMIN_PASS=change_me" Environment="MAIL_ADMIN_HOST=0.0.0.0" Environment="MAIL_ADMIN_PORT=8000" Environment="MAIL_ADMIN_ALLOW_DEBUG=false" Environment="MAIL_ADMIN_HEALTH_LOGS_LIMIT=50" # Virtual environment and Python executable ExecStart=python3 -m mail_admin # Restart policy Restart=always RestartSec=5 # Security NoNewPrivileges=false PrivateTmp=true # Logging StandardOutput=journal StandardError=journal SyslogIdentifier=mail-admin-api [Install] WantedBy=multi-user.target
-
III. Enable and start the service:
# Enable service to start on boot sudo systemctl enable mail-admin.service # Start service now sudo systemctl start mail-admin.service
Authentication: All endpoints require HTTP Basic Authentication
-
Check API health and view recent errors
-
List all mail users
-
Create a new mail user
- Creates Dovecot user entry
- Generates Maildir structure
- Adds Postfix virtual mapping
- Auto-generates 20-character password if not provided
Example (body):
{ "email": "[email protected]", "password": "optional_custom_password" } -
Delete a mail user
Example:
DELETE /users/[email protected] -
Update user password
Example (body):
{ "password": "new_secure_password" }
-
Get all LDA routing rules
-
Add a new routing rule
Example (body):
{ "pattern": "sales@*", "destination": "[email protected]" } -
Replace all routing rules
-
Delete a routing rule by index
Example:
DELETE /routing/rules/0
-
View aggregated logs from mail_admin, Dovecot, Postfix, and Fetchmail
Query Parameters:
-
page- Page number (0-indexed, default: 0) -
page_size- Items per page (default: 50, max: 500) -
level- Filter by level:DEBUG,INFO,WARNING,ERROR,CRITICAL -
sources- Filter by sources (comma-separated):mail_admin,dovecot,postfix,fetchmailExamples:
# Get recent errors GET /logs?level=ERROR&page_size=20 # View Postfix logs only GET /logs?sources=postfix # Pagination GET /logs?page=2&page_size=100
-
-
List all configured webhooks, optionally filtered by group
Example:
GET /webhooksExample (group filter):
GET /webhooks?group=notifications -
Get details of a specific webhook
Example:
GET /webhooks/550e8400-e29b-41d4-a716-446655440000 -
Create a new webhook that triggers when an email pattern matches in logs
Example (body):
{ "name": "Support Email Alerts", "email_pattern": "support@example\\.com", "webhook_url": "https://your-service.com/webhook", "enabled": true }Parameters:
name- Descriptive name for the webhookemail_pattern- Regex pattern to match email addresses in logswebhook_url- URL to POST to when pattern matchesenabled- Whether the webhook is active (default: true)
-
Update an existing webhook (all fields optional)
Example:
PATCH /webhooks/550e8400-e29b-41d4-a716-446655440000 -
Delete a webhook
Example:
DELETE /webhooks/550e8400-e29b-41d4-a716-446655440000
When a webhook is triggered, the following JSON is POST-ed to the configured URL:
{
"webhook_id": "550e8400-e29b-41d4-a716-446655440000",
"webhook_name": "Support Email Alerts",
"email": "[email protected]",
"log_entry": "[postfix] Message received from [email protected]",
"timestamp": "2025-12-08T10:30:45.123456",
"pattern": "support@example\\.com"
}For real-time webhook notifications when emails arrive, you can integrate webhooks directly into your LDA (Local Delivery Agent) script. This allows webhooks to fire immediately when an email is delivered.
-
Trigger webhooks immediately from the LDA script (localhost only, no authentication required)
Security: This endpoint only accepts requests from localhost (127.0.0.1, ::1) for security. External requests will be rejected with 403 Forbidden.
Example:
curl -X POST http://localhost:8000/webhooks/lda-trigger \ -H "Content-Type: application/json" \ -d '{ "recipient": "[email protected]", "sender": "[email protected]", "subject": "New message" }'
Parameters:
-
recipient- The email address receiving the message (required) -
sender- The sender's email address (optional) -
subject- The email subject line (optional)Note: All webhooks matching the recipient's email pattern will be triggered, regardless of their group.
Response:
{ "status": "success", "triggered_count": 2, "message": "Triggered 2 webhook(s)" }
-
Add webhook triggering to your lda-alias script (usually in /usr/local/bin/lda-alias):
#!/bin/bash
# Configuration
WEBHOOK_ENABLED=true
WEBHOOK_URL="http://localhost:8000/webhooks/lda-trigger"
# Get email details from environment or arguments
RECIPIENT="${RECIPIENT:-$1}"
SENDER="${SENDER:-unknown}"
SUBJECT="${SUBJECT:-No subject}"
# ... your existing LDA stuff here ...
# Trigger webhooks after successful delivery
if [ "$WEBHOOK_ENABLED" = true ]; then
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"recipient\": \"$RECIPIENT\",
\"sender\": \"$SENDER\",
\"subject\": \"$SUBJECT\"
\"group\": \"$WEBHOOK_GROUP\"
}" > /dev/null 2>&1 &
fi-
Create user:
curl -X POST http://localhost:8000/users \ -u admin:password \ -H "Content-Type: application/json" \ -d '{"email": "[email protected]"}'
-
View error logs:
curl -X GET "http://localhost:8000/logs?level=ERROR&page_size=10" \ -u admin:password | jq
-
Add routing rule:
curl -X POST http://localhost:8000/routing/rules \ -u admin:password \ -H "Content-Type: application/json" \ -d '{ "pattern": "support@*.example.com", "destination": "[email protected]" }'
-
List all users:
curl -X GET http://localhost:8000/users \ -u admin:password | jq -
Create webhook:
curl -X POST http://localhost:8000/webhooks \ -u admin:password \ -H "Content-Type: application/json" \ -d '{ "name": "New User Notifications", "email_pattern": ".*@newdomain\\.com", "webhook_url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", "enabled": true }'
-
List webhooks:
curl -X GET http://localhost:8000/webhooks \ -u admin:password | jq
This project is open source. Feel free to use and modify as needed.