Files
mail-autoconfig/README.md

6.7 KiB
Raw Permalink Blame History

Mail Autoconfig

A single HTTP service that provides Thunderbird autoconfig and Outlook autodiscover for multiple domains. Email clients discover IMAP/SMTP settings by requesting well-known URLs; this app serves the correct XML based on the request host (e.g. autoconfig.example.com or autodiscover.example.com).

Features

  • Thunderbird (Mozilla) — Serves config-v1.1.xml at /mail/config-v1.1.xml when the client requests https://autoconfig.<domain>/mail/config-v1.1.xml.
  • Outlook (Microsoft) — Serves Autodiscover XML at /Autodiscover/Autodiscover.xml when the client requests https://autodiscover.<domain>/Autodiscover/Autodiscover.xml (GET or POST).
  • Multi-domain — One deployment serves many domains; the Host header selects the domain. Each domain must be in the allowlist.
  • Security — Host allowlist, domain sanitization, rate limiting, security headers, and a hardened Docker setup (see Security).

How it works

  1. Thunderbird looks up the autoconfig URL for the users domain (e.g. user@example.comhttps://autoconfig.example.com/mail/config-v1.1.xml). It requests that URL; the app returns XML with IMAP/SMTP host, ports, and auth.
  2. Outlook does the same for autodiscover (e.g. https://autodiscover.example.com/Autodiscover/Autodiscover.xml). The app returns Microsofts Autodiscover XML with server settings.

The app derives the “domain” from the Host header (e.g. autoconfig.example.comexample.com). The domain is validated against an allowlist and sanitized; it is only used in the XML for display/identification. IMAP and SMTP server hostnames and ports are currently fixed in the templates (e.g. mail.mifi.holdings, 993/587). To use different mail servers per domain, you would extend the templates or config.

Endpoints

Endpoint Methods Description Rate limit
/ GET Landing page with links to autoconfig/autodiscover URLs for the current host 50/hour
/ping GET Health check (returns plain text) 10/minute
/mail/config-v1.1.xml GET Thunderbird autoconfig XML 20/hour
/Autodiscover/Autodiscover.xml GET, POST Outlook autodiscover XML 20/hour

All endpoints require a Host header that matches the allowlist (e.g. autoconfig.example.com, autodiscover.example.com). Unlisted hosts receive 403 Forbidden.

Supported domains

Domains are configured in the ALLOWED_HOSTS set in app.py. Each domain must have both forms:

  • autoconfig.<domain>
  • autodiscover.<domain>

To add a domain, add both hostnames to ALLOWED_HOSTS and, if you use Traefik, add matching router/service labels in docker-compose.yml (see Deployment).

Requirements

  • Python 3.11+ (for local run)
  • Dependencies: Flask, Jinja2, Gunicorn (see requirements.txt)
  • Production: Docker; reverse proxy (e.g. Traefik) for TLS and routing

Installation

Build and run with Docker Compose. The app listens on port 8080 inside the container and is intended to sit behind a reverse proxy (e.g. Traefik) that terminates TLS and routes by host.

docker build -t mail-autoconfig .
docker run --rm -p 8080:8080 mail-autoconfig

For production you typically use docker-compose.yml, which wires the service into a Traefik network and applies security and resource limits.

Docker Compose (production)

  1. Ensure the marina-net (or your Traefik network) exists:
    docker network create marina-net
  2. Update docker-compose.yml if your image name or network differs.
  3. Run:
    docker compose up -d

The Compose file configures Traefik routers so that each allowed host (e.g. autoconfig.mifi.holdings) is routed to this service on port 8080. TLS is handled by Traefik (e.g. Lets Encrypt).

Local development

python -m venv .venv
source .venv/bin/activate   # or .venv\Scripts\activate on Windows
pip install -r requirements.txt
flask --app app run --port 5000

Then open e.g. http://localhost:5000/ with a Host header set to an allowed host (e.g. autoconfig.mifi.holdings), or use a hosts file and Host: autoconfig.mifi.holdings. The app binds to all interfaces; behind a proxy it expects X-Forwarded-For and X-Forwarded-Proto from a trusted proxy (see Security).

Configuration

  • Allowed hosts — Edit ALLOWED_HOSTS in app.py and add corresponding Traefik labels in docker-compose.yml for each host.
  • Mail server — IMAP/SMTP hostnames and ports are in the Jinja2 templates under templates/ (config-v1.1.xml.j2, Autodiscover.xml.j2). Change those to point to your mail server(s).
  • Rate limits — Defaults are in app.py (e.g. 50 req/h for /, 20 req/h for config endpoints). Adjust the @rate_limit decorator arguments if needed.
  • Traefik — Ensure Traefik sets the real client IP (e.g. overwrites or sets X-Forwarded-For) so rate limiting and logging use the correct IP.

Security

The service is hardened for production: host validation, domain sanitization, rate limiting, request size limits, security headers (CSP, HSTS, etc.), and a read-only, non-root container with minimal capabilities. Details and audit history are in:

Important: Only attach trusted containers to the same Docker network as this service. Ensure the reverse proxy overwrites X-Forwarded-For with the real client IP.

CI/CD (Woodpecker)

Pipelines are in .woodpecker/:

  • build — On push to main: build and push Docker image to git.mifi.dev/mifi-holdings/mail-autoconfig, then notify.
  • production — On deployment to production: trigger Portainer stack webhook, then notify.

See docs/CI-CD.md for Woodpecker setup and migration from Drone.

Project layout

.
├── app.py              # Flask app, routes, security (host check, rate limit, sanitization)
├── templates/          # Jinja2 templates for Thunderbird and Outlook XML
│   ├── config-v1.1.xml.j2
│   └── Autodiscover.xml.j2
├── requirements.txt    # Pinned Python dependencies
├── Dockerfile          # Multi-stage not used; single stage, non-root user
├── docker-compose.yml  # Service + Traefik labels and security options
├── .woodpecker/        # Woodpecker CI workflows
└── docs/
    ├── CI-CD.md        # CI/CD setup and migration
    ├── SECURITY.md     # Security summary
    └── SECURITY-AUDIT.md

License

Not specified in the repo; treat as proprietary unless stated otherwise.