This document describes the HTTP API for Pony Mail Foal. All endpoints accept JSON request bodies (POST) and return JSON unless otherwise noted.
The formal OpenAPI 3.0 specification is available at
server/openapi.yaml.
- Authentication
- Endpoints
- stats.json — Search/list emails
- email.json — Fetch a single email
- thread.json — Fetch an email thread
- source.json — Fetch raw email source
- mbox.json — Download mbox archive
- compose.json — Send an email
- preferences.json — User preferences and list overview
- mgmt.json — Administrative operations
- pminfo.json — Server activity info
- gravatar.json — Avatar image proxy
- plain.json — Plain HTML for search engines
- Common Parameters
- Differences from Legacy PonyMail API
Foal uses cookie-based sessions via OAuth. The session cookie is named
ponymail. Most read endpoints work without authentication for public
lists. Private list access and write operations (compose, management)
require an authenticated session via an authoritative OAuth provider.
Search the archives and return matching results.
POST /api/stats.json
| Parameter | Type | Required | Description |
|---|---|---|---|
list |
string | yes | List name prefix (e.g. dev). Use * for wildcard. |
domain |
string | yes | List domain (e.g. httpd.apache.org). Use * for wildcard. |
d |
string | no | Date/timespan (see below) |
s |
string | no | Start month (yyyy-mm) |
e |
string | no | End month (yyyy-mm) |
dfrom |
string | no | Start date as days ago |
dto |
string | no | Number of days to include from dfrom |
q |
string | no | Free-text search query (see syntax) |
header_from |
string | no | Filter by From: header |
header_to |
string | no | Filter by To: header |
header_subject |
string | no | Filter by Subject: header |
header_body |
string | no | Filter by message body |
header_messageid |
string | no | Filter by Message-ID: header |
quick |
(presence) | no | Return statistics only (omit emails, thread_struct, word cloud, participants) |
emailsOnly |
(presence) | no | Return email summaries only (omit thread_struct, participants, word cloud) |
since |
integer | no | UNIX epoch; returns {"changed": false} if no emails are newer |
{
"hits": 134,
"numparts": 28,
"no_threads": 35,
"firstYear": 2018,
"firstMonth": 1,
"lastYear": 2021,
"lastMonth": 11,
"name": "dev",
"domain": "lists.example.org",
"list": "dev@lists.example.org",
"searchlist": "<dev.lists.example.org>",
"active_months": [{"2021-01": 15}, {"2021-02": 23}],
"emails": [ /* array of CompactEmailResponse */ ],
"thread_struct": [ /* threaded representation */ ],
"participants": [
{"email": "jane@example.org", "name": "Jane Doe", "count": 10, "gravatar": "..."}
],
"cloud": {"word1": 25, "word2": 10},
"searchParams": {"list": "dev", "domain": "lists.example.org", "d": "gte=2018-01"},
"unixtime": 1506761839
}curl -X POST https://lists.apache.org/api/stats.json \
-H "Content-Type: application/json" \
-d '{"list": "dev", "domain": "ponymail.apache.org", "d": "lte=3M"}'Fetch a single email by permalink ID or Message-ID.
POST /api/email.json
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Email permalink ID or Message-ID header value |
listid |
string | conditional | Required when looking up by Message-ID (for disambiguation) |
attachment |
boolean | no | Set to true to fetch an attachment |
file |
string | no | Attachment hash (required when attachment=true) |
{
"id": "r8cmj7vm5n8z5r3xda5ebd",
"mid": "r8cmj7vm5n8z5r3xda5ebd",
"dbid": "08c4e61930db221d...",
"message-id": "<521062724.28.1506761839312.JavaMail.jenkins@host>",
"from": "Jane Doe <jane@example.org>",
"from_raw": "Jane Doe <jane@example.org>",
"to": "dev@example.org",
"cc": "announce@example.org",
"subject": "Re: weekly meeting",
"date": "2017/09/30 08:57:19",
"epoch": 1506761839,
"list": "<dev.example.org>",
"list_raw": "<dev.example.org>",
"body": "Full message body...",
"body_short": "Truncated to 201 chars...",
"private": false,
"references": "<parent-message-id>",
"in-reply-to": "<parent-message-id>",
"attachments": [],
"permalinks": ["r8cmj7vm5n8z5r3xda5ebd", "..."],
"gravatar": "69eea47c5083c2e4945a2704fc7b658c"
}Notes:
dateandepochare in UTC.- When
attachment=trueand a matchingfilehash is found, the raw attachment binary is returned with appropriate Content-Type and Content-Disposition headers.
Fetch a complete email thread starting from a given email.
POST /api/thread.json
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Email permalink ID or Message-ID |
listid |
string | no | List-ID for disambiguation when using Message-ID |
find_parent |
boolean | no | If true, navigate up to the thread root before fetching |
{
"thread": {
"from": "...",
"subject": "...",
"id": "...",
"epoch": 1506761839,
"children": [ /* nested CompactEmailResponse objects */ ]
},
"emails": [ /* flat array of all emails in the thread */ ]
}Fetch the raw mbox source of an email.
POST /api/source.json
Same as email.json (id, optional listid).
Returns the raw RFC 2822 email source as text/plain. This includes all
original headers and the unmodified message body.
Returns HTTP 404 if the email is not found.
Download a set of emails in mbox format.
POST /api/mbox.json
Same as stats.json — all search/date parameters apply.
Returns the matching emails as a single mbox-format file (text/plain).
Compose and send an email to a list. Requires authentication via an authoritative OAuth provider.
POST /api/compose.json
| Parameter | Type | Required | Description |
|---|---|---|---|
to |
string | yes | Recipient address (must match sender_domains config) |
subject |
string | yes | Email subject |
body |
string | yes | Email message body |
references |
string | no | Message-ID reference (if not a direct reply) |
in-reply-to |
string | no | Message-ID of the email being directly replied to |
{"okay": true, "message": "Email dispatched"}Note: The sender_domains configuration controls which recipient
domains are permitted. See INSTALL.md.
Fetch user preferences, list overview, and OAuth provider configuration.
POST /api/preferences.json
| Parameter | Type | Required | Description |
|---|---|---|---|
oauth |
boolean | no | If true, return only OAuth provider configuration |
{
"login": {
"credentials": {"fullname": "Jane Doe", "email": "jane@example.org"}
},
"lists": {
"httpd.apache.org": {"dev": 1523, "users": 890},
"ponymail.apache.org": {"dev": 36}
},
"versions": {
"foal": "abc123",
"server": "def456",
"elasticsearch_engine": "8.11.0",
"elasticsearch_library": "8.11.0"
}
}Notes:
versions.server,elasticsearch_engine, andelasticsearch_libraryare only returned for authenticated users (admin-only for OpenSearch versions).- When
oauth=true, returns the configured OAuth providers for the login UI.
Administrative endpoint for email management (GDPR operations). Requires admin authentication.
POST /api/mgmt.json
| Parameter | Type | Required | Description |
|---|---|---|---|
action |
string | yes | One of: log, delete, hide, unhide, edit |
document |
string | no | Single document permalink ID |
documents |
array | no | Array of document permalink IDs (batch operations) |
size |
integer | no | Number of audit log entries (for action=log, default: 50) |
page |
integer | no | Page offset for audit log |
filter |
string | no | Filter audit log by action type |
Actions:
log— View the audit log of past admin actionsdelete— Permanently delete emails (ifallow_deleteis configured) or hide themhide— Hide emails from public view (recoverable)unhide— Restore previously hidden emailsedit— Edit email metadata (list-id, etc.)
Varies by action. For log:
{"entries": [ /* audit log entries */ ]}For mutations: returns an ActionResponse with okay and message.
Return server activity statistics. No authentication required.
POST /api/pminfo.json
Returns the server's gathered activity data (list counts, processing stats).
Caching proxy for Gravatar images.
POST /api/gravatar.json
| Parameter | Type | Required | Description |
|---|---|---|---|
md5 |
string | yes | MD5 hash of the email address (lowercased) |
Returns a image/png response with 24-hour cache headers. Falls back to
a default avatar if the hash is unknown.
Plain HTML rendering for search engine indexing.
This endpoint serves publicly available lists and threads as simple HTML with canonical link elements, enabling search engines to index the archive content and link to the standard JS-based UI URLs.
The d parameter supports several formats:
| Format | Meaning | Example |
|---|---|---|
yyyy-mm |
Specific month | 2021-06 |
lte=N[wMyd] |
Less than N weeks/Months/years/days ago | lte=3M |
gte=N[wMyd] |
More than N weeks/Months/years/days ago | gte=1y |
dfr=yyyy-mm-dd|dto=yyyy-mm-dd |
Date range (inclusive) | dfr=2021-09-01|dto=2021-09-30 |
The s and e parameters provide an alternative way to specify a
month range: s=2021-01&e=2021-06.
The dfrom/dto pair specifies days: dfrom=31 (31 days ago) with
dto=10 (10 days of data starting from that point).
Units: w = weeks, M = Months, y = years, d = days.
lte and gte are mutually exclusive. dfr and dto are normally
used together.
The q parameter supports:
| Syntax | Meaning | Example |
|---|---|---|
word |
Must contain word | apples |
+word |
Word must be present | +oranges |
-word |
Word must NOT be present | -bananas |
"phrase" |
Exact phrase match | "weekly meeting" |
Additional filters can narrow results:
header_from— match sender addressheader_to— match recipient addressheader_subject— match subject lineheader_body— match message body onlyheader_messageid— match Message-ID header
Foal's API is largely compatible with the original Lua-based PonyMail, with the following notable differences:
| Change | Details |
|---|---|
| Endpoint suffix | Foal uses .json (e.g. /api/stats.json) instead of .lua |
| Method | All endpoints use POST with JSON body (legacy used GET with query params) |
notifications.lua |
Not available in Foal |
atom.lua |
Not available in Foal |
| Additional email fields | dbid, permalinks, body_short, from_raw, list_raw are new in Foal |
find_parent |
New parameter on thread.json to navigate to thread root |
versions in preferences |
New — shows Foal, server, and OpenSearch version info |
mgmt.json |
New — admin/GDPR management endpoint (not in legacy PM) |
gravatar.json |
New — caching proxy (legacy embedded gravatar handling differently) |
plain.json |
New — search engine indexing support |
- OpenAPI Specification — formal schema definition
- Installation Guide — setup and configuration
- Server README — running the backend