| <!--- |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --> |
| |
| # Installing PonyMail Foal |
| |
| 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. |
| |
| --- |
| |
| ## Prerequisites |
| |
| | 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 | |
| |
| --- |
| |
| ## Step 1: Install OpenSearch |
| |
| ### RHEL / Alma Linux 9 |
| |
| ```bash |
| # 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 |
| ``` |
| |
| ### Debian / Ubuntu |
| |
| ```bash |
| 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 |
| ``` |
| |
| ### Configure and start |
| |
| For a single-node development install, disable security (TLS/auth) |
| to simplify setup: |
| |
| ```bash |
| # /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: |
| |
| ```bash |
| curl http://localhost:9200/ |
| # Should return a JSON blob with version info |
| ``` |
| |
| --- |
| |
| ## Step 2: Clone PonyMail Foal |
| |
| ```bash |
| git clone https://github.com/apache/incubator-ponymail-foal.git /opt/ponymail |
| cd /opt/ponymail |
| ``` |
| |
| --- |
| |
| ## Step 3: Install Python Dependencies |
| |
| ```bash |
| 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.txt` pins |
| > `elasticsearch[async]>=7.13.1,<7.14.0` because 7.14+ introduces |
| > strict server version checking. This version works with both |
| > OpenSearch 2.x (the `elasticsearch` Python client is compatible |
| > with both without code changes). |
| |
| --- |
| |
| ## Step 4: Run Setup |
| |
| The setup script creates the database indices and generates your |
| configuration files. |
| |
| ```bash |
| cd /opt/ponymail/tools |
| python3 setup.py |
| ``` |
| |
| It will ask you: |
| - **OpenSearch/OpenSearch URL**: `http://localhost:9200/` (default) |
| - **Index prefix**: `ponymail` (default) |
| - **Outgoing mail server**: your SMTP host (or `localhost` if local) |
| - **Accepted domains**: domains you'll allow web replies to (or `*`) |
| - **Word cloud**: Y/N |
| - **ID generator**: choose `dkim` (recommended) |
| - **Shards/replicas**: 1 shard, 0 replicas for single-node dev |
| |
| This creates: |
| - `tools/archiver.yaml` — archiver configuration |
| - `server/ponymail.yaml` — server configuration |
| |
| For automated/non-interactive setup: |
| |
| ```bash |
| python3 setup.py --devel # Single-node dev defaults (1 shard, 0 replicas) |
| python3 setup.py --defaults # Production defaults (3 shards, 1 replica) |
| ``` |
| |
| --- |
| |
| ## Step 5: Import Some Mail |
| |
| You need mbox files to populate the archive. You can download from an |
| existing PonyMail instance: |
| |
| ```bash |
| 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: |
| |
| ```bash |
| python3 import-mbox.py --source /var/mail/lists/dev.mbox --lid dev@yourproject.org |
| ``` |
| |
| --- |
| |
| ## Step 6: Start the API Server |
| |
| ```bash |
| 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](configuration.md)). |
| |
| For production, create a systemd unit: |
| |
| ```ini |
| # /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 |
| ``` |
| |
| ```bash |
| systemctl enable --now ponymail |
| ``` |
| |
| --- |
| |
| ## Step 7: Configure the Web Server |
| |
| The web UI is static HTML/JS in `webui/`. The API server runs |
| separately and needs to be proxied. |
| |
| ### Apache httpd |
| |
| ```apache |
| <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> |
| ``` |
| |
| --- |
| |
| ## Step 8: Set Up Archiving (Live Email) |
| |
| To archive incoming mail in real time, pipe it to the archiver from |
| your MTA. |
| |
| ### Postfix |
| |
| 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" |
| ``` |
| |
| --- |
| |
| ## Step 9: Verify |
| |
| Browse to `http://lists.example.org/` — you should see the list |
| overview with any lists you've imported. |
| |
| --- |
| |
| ## Optional: OAuth Authentication |
| |
| OAuth is needed for: |
| - Viewing private lists |
| - Composing replies via the web UI |
| - Admin management console |
| |
| See the [configuration reference](configuration.md#oauth) for |
| provider setup (Google, GitHub, or generic OAuth). |
| |
| Quick checklist: |
| 1. Register an OAuth app with your provider |
| 2. Set `client_id` (and `client_secret` for GitHub) in `server/ponymail.yaml` |
| 3. Set the same `client_id` in `webui/js/config.js` |
| 4. Add your provider's domain to `authoritative_domains` |
| 5. Add admin email addresses to `admins` if you want management console access |
| |
| --- |
| |
| ## Troubleshooting |
| |
| ### "Connection refused" on port 9200 |
| |
| OpenSearch isn't running. Check: |
| ```bash |
| 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). |
| |
| ### Import hangs or is very slow |
| |
| Check that OpenSearch is healthy: |
| ```bash |
| 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: |
| ```bash |
| curl -X DELETE http://localhost:9200/ponymail-* |
| cd /opt/ponymail/tools && python3 setup.py --devel |
| ``` |
| |
| ### API returns "API Endpoint not found!" |
| |
| 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. |
| |
| ### Web UI shows but no lists appear |
| |
| The background indexer hasn't run yet. Wait 2–3 minutes (controlled |
| by `tasks.refresh_rate` in ponymail.yaml), or restart the server. |
| |
| --- |
| |
| ## Next Steps |
| |
| - [Configuration Reference](configuration.md) — all `ponymail.yaml` options |
| - [User Guide](user_guide.md) — how to use the web interface |
| - [Admin Guide](admin_guide.md) — management console and GDPR operations |
| - [API Documentation](API.md) — HTTP API reference |