A step-by-step guide to getting PonyMail Foal running from scratch. This guide uses OpenSearch (the Apache 2.0-licensed fork of OpenSearch). OpenSearch 2.x works identically — just substitute the package names.
| Component | Version | Notes |
|---|---|---|
| OS | Linux (tested: Alma/RHEL 9, Debian 12, Ubuntu 22.04+) | macOS works for dev |
| Python | 3.8+ | 3.11+ recommended |
| OpenSearch | 2.x (or ElasticSearch 7.x) | API-compatible; Foal uses the elasticsearch Python client |
| Web server | Apache httpd or nginx | Reverse proxy for the API + static file serving |
| SMTP | Any MTA (Postfix, sendmail) | Only needed if you want web-based replies |
# Import GPG key and add repo curl -SL https://artifacts.opensearch.org/publickeys/opensearch.pgp | \ gpg --dearmor -o /usr/share/keyrings/opensearch-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/opensearch-keyring.gpg] \ https://artifacts.opensearch.org/releases/bundle/opensearch/2.x/yum" \ > /etc/yum.repos.d/opensearch.repo # Or use the RPM directly: curl -SL https://artifacts.opensearch.org/releases/bundle/opensearch/2.17.0/opensearch-2.17.0-linux-x64.rpm \ -o opensearch.rpm rpm -ivh opensearch.rpm
curl -SL https://artifacts.opensearch.org/publickeys/opensearch.pgp | \ gpg --dearmor -o /usr/share/keyrings/opensearch-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/opensearch-keyring.gpg] \ https://artifacts.opensearch.org/releases/bundle/opensearch/2.x/apt stable main" \ | tee /etc/apt/sources.list.d/opensearch.list apt update && apt install opensearch
For a single-node development install, disable security (TLS/auth) to simplify setup:
# /etc/opensearch/opensearch.yml cat >> /etc/opensearch/opensearch.yml << 'EOF' discovery.type: single-node plugins.security.disabled: true EOF systemctl enable --now opensearch
Verify it's running:
curl http://localhost:9200/ # Should return a JSON blob with version info
git clone https://github.com/apache/incubator-ponymail-foal.git /opt/ponymail cd /opt/ponymail
cd /opt/ponymail/tools pip install -r requirements.txt cd /opt/ponymail/server pip install -r requirements.txt
Note on OpenSearch client version: The
requirements.txtpinselasticsearch[async]>=7.13.1,<7.14.0because 7.14+ introduces strict server version checking. This version works with both OpenSearch 2.x (theelasticsearchPython client is compatible with both without code changes).
The setup script creates the database indices and generates your configuration files.
cd /opt/ponymail/tools python3 setup.py
It will ask you:
http://localhost:9200/ (default)ponymail (default)localhost if local)*)dkim (recommended)This creates:
tools/archiver.yaml — archiver configurationserver/ponymail.yaml — server configurationFor automated/non-interactive setup:
python3 setup.py --devel # Single-node dev defaults (1 shard, 0 replicas) python3 setup.py --defaults # Production defaults (3 shards, 1 replica)
You need mbox files to populate the archive. You can download from an existing PonyMail instance:
cd /opt/ponymail/tools # Download a month of a public list curl -o dev_community.mbox \ "https://lists.apache.org/api/mbox.lua?list=dev&domain=community.apache.org&date=2025-01" # Import it python3 import-mbox.py --source dev_community.mbox
Or import a local mbox file:
python3 import-mbox.py --source /var/mail/lists/dev.mbox --lid dev@yourproject.org
cd /opt/ponymail/server python3 main.py
The server listens on 127.0.0.1:8080 by default. Edit server/ponymail.yaml to change the bind address or port (see configuration reference).
For production, create a systemd unit:
# /etc/systemd/system/ponymail.service [Unit] Description=Apache Pony Mail Foal API Server After=network.target opensearch.service [Service] Type=simple User=ponymail WorkingDirectory=/opt/ponymail/server ExecStart=/usr/bin/python3 main.py Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
systemctl enable --now ponymail
The web UI is static HTML/JS in webui/. The API server runs separately and needs to be proxied.
<VirtualHost *:80> ServerName lists.example.org DocumentRoot /opt/ponymail/webui # Proxy API requests to the Python backend ProxyPass /api/ http://127.0.0.1:8080/api/ ProxyPassReverse /api/ http://127.0.0.1:8080/api/ # Required for thread URLs containing encoded slashes AllowEncodedSlashes On # Required for /thread/message-id path info AcceptPathInfo On <Directory /opt/ponymail/webui> Require all granted Options +MultiViews </Directory> </VirtualHost>
To archive incoming mail in real time, pipe it to the archiver from your MTA.
Edit /etc/aliases:
mylist: "| /opt/ponymail/tools/archiver.py"
Run newaliases after editing.
The archiver reads List-ID headers to determine which list the email belongs to. You can override with --lid:
mylist: "| /opt/ponymail/tools/archiver.py --lid mylist@example.org"
For private lists:
private-list: "| /opt/ponymail/tools/archiver.py --private"
Browse to http://lists.example.org/ — you should see the list overview with any lists you've imported.
OAuth is needed for:
See the configuration reference for provider setup (Google, GitHub, or generic OAuth).
Quick checklist:
client_id (and client_secret for GitHub) in server/ponymail.yamlclient_id in webui/js/config.jsauthoritative_domainsadmins if you want management console accessOpenSearch isn't running. Check:
systemctl status opensearch journalctl -u opensearch --no-pager -n 50
Common cause: insufficient heap memory. Edit /etc/opensearch/jvm.options and set -Xms512m -Xmx512m (minimum).
Check that OpenSearch is healthy:
curl http://localhost:9200/_cluster/health
If status is red, you may have unassigned shards from a previous failed setup. Delete and re-run setup:
curl -X DELETE http://localhost:9200/ponymail-* cd /opt/ponymail/tools && python3 setup.py --devel
The request path is wrong. Foal expects /api/{endpoint}.json (POST) or /api/{endpoint}.lua (GET). Check your proxy config is passing the full path through.
The background indexer hasn't run yet. Wait 2–3 minutes (controlled by tasks.refresh_rate in ponymail.yaml), or restart the server.
ponymail.yaml options