Apache Pony Mail (Foal) — API Documentation

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.


Table of Contents


Authentication

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.


Endpoints

stats.json

Search the archives and return matching results.

POST /api/stats.json

Request Parameters

ParameterTypeRequiredDescription
liststringyesList name prefix (e.g. dev). Use * for wildcard.
domainstringyesList domain (e.g. httpd.apache.org). Use * for wildcard.
dstringnoDate/timespan (see below)
sstringnoStart month (yyyy-mm)
estringnoEnd month (yyyy-mm)
dfromstringnoStart date as days ago
dtostringnoNumber of days to include from dfrom
qstringnoFree-text search query (see syntax)
header_fromstringnoFilter by From: header
header_tostringnoFilter by To: header
header_subjectstringnoFilter by Subject: header
header_bodystringnoFilter by message body
header_messageidstringnoFilter by Message-ID: header
quick(presence)noReturn statistics only (omit emails, thread_struct, word cloud, participants)
emailsOnly(presence)noReturn email summaries only (omit thread_struct, participants, word cloud)
sinceintegernoUNIX epoch; returns {"changed": false} if no emails are newer

Response (StatsResponse)

{
  "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
}

Example

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"}'

email.json

Fetch a single email by permalink ID or Message-ID.

POST /api/email.json

Request Parameters

ParameterTypeRequiredDescription
idstringyesEmail permalink ID or Message-ID header value
listidstringconditionalRequired when looking up by Message-ID (for disambiguation)
attachmentbooleannoSet to true to fetch an attachment
filestringnoAttachment hash (required when attachment=true)

Response (SingleEmailResponse)

{
  "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:

  • date and epoch are in UTC.
  • When attachment=true and a matching file hash is found, the raw attachment binary is returned with appropriate Content-Type and Content-Disposition headers.

thread.json

Fetch a complete email thread starting from a given email.

POST /api/thread.json

Request Parameters

ParameterTypeRequiredDescription
idstringyesEmail permalink ID or Message-ID
listidstringnoList-ID for disambiguation when using Message-ID
find_parentbooleannoIf true, navigate up to the thread root before fetching

Response (ThreadResponse)

{
  "thread": {
    "from": "...",
    "subject": "...",
    "id": "...",
    "epoch": 1506761839,
    "children": [ /* nested CompactEmailResponse objects */ ]
  },
  "emails": [ /* flat array of all emails in the thread */ ]
}

source.json

Fetch the raw mbox source of an email.

POST /api/source.json

Request Parameters

Same as email.json (id, optional listid).

Response

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.


mbox.json

Download a set of emails in mbox format.

POST /api/mbox.json

Request Parameters

Same as stats.json — all search/date parameters apply.

Response

Returns the matching emails as a single mbox-format file (text/plain).


compose.json

Compose and send an email to a list. Requires authentication via an authoritative OAuth provider.

POST /api/compose.json

Request Parameters

ParameterTypeRequiredDescription
tostringyesRecipient address (must match sender_domains config)
subjectstringyesEmail subject
bodystringyesEmail message body
referencesstringnoMessage-ID reference (if not a direct reply)
in-reply-tostringnoMessage-ID of the email being directly replied to

Response (ActionResponse)

{"okay": true, "message": "Email dispatched"}

Note: The sender_domains configuration controls which recipient domains are permitted. See INSTALL.md.


preferences.json

Fetch user preferences, list overview, and OAuth provider configuration.

POST /api/preferences.json

Request Parameters

ParameterTypeRequiredDescription
oauthbooleannoIf true, return only OAuth provider configuration

Response

{
  "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, and elasticsearch_library are only returned for authenticated users (admin-only for OpenSearch versions).
  • When oauth=true, returns the configured OAuth providers for the login UI.

mgmt.json

Administrative endpoint for email management (GDPR operations). Requires admin authentication.

POST /api/mgmt.json

Request Parameters

ParameterTypeRequiredDescription
actionstringyesOne of: log, delete, hide, unhide, edit
documentstringnoSingle document permalink ID
documentsarraynoArray of document permalink IDs (batch operations)
sizeintegernoNumber of audit log entries (for action=log, default: 50)
pageintegernoPage offset for audit log
filterstringnoFilter audit log by action type

Actions:

  • log — View the audit log of past admin actions
  • delete — Permanently delete emails (if allow_delete is configured) or hide them
  • hide — Hide emails from public view (recoverable)
  • unhide — Restore previously hidden emails
  • edit — Edit email metadata (list-id, etc.)

Response

Varies by action. For log:

{"entries": [ /* audit log entries */ ]}

For mutations: returns an ActionResponse with okay and message.


pminfo.json

Return server activity statistics. No authentication required.

POST /api/pminfo.json

Returns the server's gathered activity data (list counts, processing stats).


gravatar.json

Caching proxy for Gravatar images.

POST /api/gravatar.json

Request Parameters

ParameterTypeRequiredDescription
md5stringyesMD5 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.json

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.


Common Parameters

Date/Timespan Parameters

The d parameter supports several formats:

FormatMeaningExample
yyyy-mmSpecific month2021-06
lte=N[wMyd]Less than N weeks/Months/years/days agolte=3M
gte=N[wMyd]More than N weeks/Months/years/days agogte=1y
dfr=yyyy-mm-dd|dto=yyyy-mm-ddDate 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.

Search Query Syntax

The q parameter supports:

SyntaxMeaningExample
wordMust contain wordapples
+wordWord must be present+oranges
-wordWord must NOT be present-bananas
"phrase"Exact phrase match"weekly meeting"

Additional filters can narrow results:

  • header_from — match sender address
  • header_to — match recipient address
  • header_subject — match subject line
  • header_body — match message body only
  • header_messageid — match Message-ID header

Differences from Legacy PonyMail API

Foal's API is largely compatible with the original Lua-based PonyMail, with the following notable differences:

ChangeDetails
Endpoint suffixFoal uses .json (e.g. /api/stats.json) instead of .lua
MethodAll endpoints use POST with JSON body (legacy used GET with query params)
notifications.luaNot available in Foal
atom.luaNot available in Foal
Additional email fieldsdbid, permalinks, body_short, from_raw, list_raw are new in Foal
find_parentNew parameter on thread.json to navigate to thread root
versions in preferencesNew — shows Foal, server, and OpenSearch version info
mgmt.jsonNew — admin/GDPR management endpoint (not in legacy PM)
gravatar.jsonNew — caching proxy (legacy embedded gravatar handling differently)
plain.jsonNew — search engine indexing support

Related Resources