Mobile-friendly REST API for PostfixAdmin alias and mailbox password management. This API was originally developed to support the PostfixMe mobile app but can be used by others.
This is a container-based overlay for the PostfixAdmin Docker image, not a plugin for PostfixAdmin. You cannot use PostfixMe without PostfixAdmin.
You may enjoy using the PostfixMe API when any of the following are true:
- Your mail server runs postfix. This isn't just for DIY (Do It Yourself) mail server administrators. If you're running postfix, you should also be running PostfixAdmin, especially if you have more than one mailbox and/or domain. Adding the PostfixMe API adds additional capabilities to PostfixAdmin that are valuable for the other reasons, here.
- You have users. Users today are less inclined to sit at a PC to manage their own mailbox passwords and mailbox aliases. Out of the box, PostfixAdmin requires users to log into its web-based interface, which performs poorly on small mobile devices. At the time of this writing, that interface is rudimentary and incomplete for non-administrators, further driving non-administrators away from using it. How much time can you take back if you aren't forced to log into the PostfixAdmin interface as an administrator to handle your user requests? Paired with a mobile app that employs the PostfixMe API, this offloads that demand, enabling your users to self-manage their own mailbox passwords and aliases, freeing your mail server administrator(s) of that burden.
- You hate spam. Mailbox aliases are an anti-spam tool. When you create and use a unique email alias for every point of contact, you take immediate and direct control over who can send you email and when. The moment you see spam to any of your aliases, you can disable/delete the compromosed alias and know with irrefutible certainty who sold you out. This is "throwaway email addresses" at scale, trivial to create, trivial to disable/destroy.
The PostfixMe API provides a secure JSON REST interface that allows mobile applications to manage email aliases and mailbox passwords for individual users through PostfixAdmin's database. It directly utilizes PostfixAdmin's password verification and configuration code. This does not extend all of the capabilities of PostfixAdmin. Rather, this API enables end-users to self-manage their own mailbox aliases and mailbox passwords via mobile app, taking such load off of your mail server administrator(s).
- Authentication: JWT-based (RS256) with access and refresh tokens
- Security: TLS enforcement, rate limiting, account lockout protection
- Alias Management: Create, read, update, delete email aliases
- Mailbox Password Management: Users can change their own mailbox password with adherence to administrator-specified password rules
- Scoped Alias Access: Users can only manage aliases that forward to their own mailbox
- Pagination: Built-in pagination support for alias listings
- Audit Logging: All authentication attempts are logged
- PHP 8.1 or higher
- PostfixAdmin database (PostgreSQL, MySQL/MariaDB, or SQLite)
- OpenSSL extension
- PDO extension
The PostfixMe API is intended for container-based deployment. Environment variables, JWT key generation, and other secret setup must be completed before deployment.
Manual installation outside of containers is not supported, though it should be reasonably simple to accomplish with sufficient investment in automation scripts.
This project provides an example Docker implementation, illustrating one possible way to deploy it along with PostfixAdmin. You can try out this sample Docker Compose stack very quickly by performing the following quickstart steps:
- Clone this project with Git submodule support (use the
--recursiveflag with yourcloneoperation). This project employs a library of generally useful Bash shell scripts which -- among other things -- simplifies otherwise very complex Docker build and deployment tasks. - Run the generate-sample-secrets.sh shell script to automatically
generate a set of environment variable (
.env) files with minimum viable configuration including randomized secrets. Review the output files (docker/*.env*) to learn what values were generated for you. These files are named for the service and deployment stage their values apply to. - Build and start the Docker Compose stack:
./build.sh --start. - Experiment with the RESTful API per the documentation below. It will be accessible at
localhostport8080unless you modify the Docker Compose YAML to change the port. Test data will have been loaded already, which you can use to authenticate and otherwise explore any of several test user accounts. The test data accounts are documented with the seed data.
Do not publish these example Docker containers. These are provided only for you to learn from in order to design and deploy your own private implementation of this PostfixMe API.
The resuling example Docker Compose stack features:
- PostfixAdmin (configured for use with a MariaDB database)
- PostfixMe RESTful API server (shares code with the PostfixAdmin service)
- MariaDB database server
- NGINX reverse proxy (single entry point for both PostfixAdmin and PostfixMe)
You are free to use the example to learn how to configure PostfixMe API for use with your existing PostfixAdmin installation, or for how to create your own Docker Compose stack which integrates them together. The example provided here is not a production-ready configuration! Rather, it is merely a minimally-viable example for a development environment.
Unit tests are located in tests/Unit/ and use PHPUnit. The test suite validates API endpoints, authentication logic,
database operations, and security controls.
Unit and integration testing is available via a single command. After you've run the initial setup above (at least
steps 1 and 2), you can run the full gammut of tests via the run-tests.sh shell script.
The codebase uses PHPStan (static analysis) and PHPCS (code style checking) to maintain quality standards. Configure these tools in your development environment as needed.
- JWT keys must be configured before running the API
- Database connection requires access to PostfixAdmin's database
- All endpoints expect and return JSON
- Rate limiting and account lockout features require persistent storage
The API requires additional tables in the PostfixAdmin database:
pfme_refresh_tokens- Stores refresh tokens with revocation supportpfme_revoked_tokens- Tracks revoked access tokens (JTI)pfme_auth_log- Audit log for authentication attemptspfme_auth_log_summary- Daily auth summary (mailbox + counts)pfme_auth_log_archive- Archived auth log records (optional)
For complete schema documentation including table structures, indexes, and migration procedures, see:
- TLS Only: The API enforces TLS by default. Only disable for local development.
- Trusted Proxies: Configure
TRUSTED_PROXY_CIDRto validate TLS headers from reverse proxies. - Rate Limiting: Failed authentication attempts are rate-limited per mailbox.
- Account Lockout: Accounts are temporarily locked after excessive failures.
- Token Revocation: Both access and refresh tokens support server-side revocation.
- Audit Logging: All authentication attempts are logged with IP and user agent.
PostfixMe logs authentication attempts to protect accounts (rate limiting, lockout, and incident investigation).
Detailed auth logs contain mailbox, timestamp, success/failure, IP address, and user agent. To balance the need for a
high degree of specificity during security incident response with user privacy concerns, these logs are anonymized after
a configurable timespan and fully deleted after another timespan provided you implement the necessary scheduler and
cleansing script, which is documented in detail in the docs/ directory.
Behavior:
- Summary: Stores only mailbox + daily counts (no IP or user agent).
- Retention: Deletes detailed logs older than the retention window.
- Archive (optional): Moves detailed logs into
pfme_auth_log_archivebefore deletion and prunes the archive by its own retention window.
Compliance Notes (confirm with your compliance team):
- GDPR: No fixed retention; use data minimization (typical 30–90 days detailed logs).
- PCI DSS: 12 months retention, 3 months immediately available (example: retention 90 days + archive 365 days).
- HIPAA: No specific auth-log duration; many organizations align to 6 years for policy retention (archive 2190 days).
- SOC 2 / ISO 27001: No prescriptive duration; adopt a documented policy (often 90–180 days detailed + summaries long-term).
The API uses environment variables for configuration. All secrets follow the *_FILE pattern.
| Variable | Default | Description |
|---|---|---|
POSTFIXADMIN_DB_HOST |
db |
Database hostname |
POSTFIXADMIN_DB_PORT |
3306 |
Database port |
POSTFIXADMIN_DB_NAME |
postfixadmin |
Database name |
POSTFIXADMIN_DB_USER_FILE |
/run/secrets/postfixadmin_db_user |
Database user secret file |
POSTFIXADMIN_DB_PASSWORD_FILE |
/run/secrets/postfixadmin_db_password |
Database password secret file |
| Variable | Default | Description |
|---|---|---|
PFME_JWT_PRIVATE_KEY_FILE |
/run/secrets/pfme_jwt_private_key |
RS256 private key file |
PFME_JWT_PUBLIC_KEY_FILE |
/run/secrets/pfme_jwt_public_key |
RS256 public key file |
PFME_ACCESS_TOKEN_TTL |
900 |
Access token lifetime (seconds, default 15 min) |
PFME_REFRESH_TOKEN_TTL |
157680000 |
Refresh token lifetime (seconds, default 5 years) |
PFME_JWT_ISSUER |
pfme-api |
JWT issuer claim |
PFME_JWT_AUDIENCE |
pfme-mobile |
JWT audience claim |
| Variable | Default | Description |
|---|---|---|
TRUSTED_PROXY_CIDR |
`` | Comma-separated CIDR blocks for trusted proxies |
TRUSTED_TLS_HEADER_NAME |
X-Forwarded-Proto |
TLS proxy header name |
PFME_REQUIRE_TLS |
true |
Enforce TLS connections |
PFME_RATE_LIMIT_ATTEMPTS |
5 |
Max failed auth attempts in window |
PFME_RATE_LIMIT_WINDOW |
300 |
Rate limit window (seconds, 5 min) |
PFME_LOCKOUT_THRESHOLD |
10 |
Failed attempts before lockout |
PFME_LOCKOUT_DURATION |
1800 |
Lockout duration (seconds, 30 min) |
PFME_PASSWORD_MIN_LENGTH |
10 |
Minimum passphrase length (8-64 recommended) |
PFME_PASSWORD_REQUIRE_SPACE |
true |
Require at least one space in passphrase |
PFME_PASSWORD_REQUIRE_GRAMMAR_SYMBOL |
true |
Require grammar symbol (. , ! ? ; : ' " - etc.) |
| Variable | Default | Description |
|---|---|---|
PFME_AUTH_LOG_RETENTION_DAYS |
90 |
Detailed log retention (days) |
PFME_AUTH_LOG_SUMMARY_ENABLED |
true |
Enable daily auth summary aggregation |
PFME_AUTH_LOG_SUMMARY_LAG_DAYS |
1 |
Days before aggregating logs into summary |
PFME_AUTH_LOG_ARCHIVE_ENABLED |
false |
Enable archiving of detailed logs before deletion |
PFME_AUTH_LOG_ARCHIVE_RETENTION_DAYS |
365 |
Archive retention (days) |
| Variable | Default | Description |
|---|---|---|
DEPLOYMENT_STAGE |
production |
Application environment (development/qa/lab/staging/production) |
Basic health check.
Response (200):
{
"status": "ok",
"timestamp": 1700000000
}Authenticate with mailbox credentials.
Request:
{
"mailbox": "user@example.com",
"password": "secret"
}Response (200):
{
"access_token": "eyJ...",
"refresh_token": "abc123...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"mailbox": "user@example.com",
"domain": "example.com"
}
}Revoke all tokens for the authenticated mailbox (requires authentication).
Headers: Authorization: Bearer <token>
Response (200):
{
"message": "Logged out successfully"
}Rotate tokens using refresh token (no access token required).
Request:
Returns the active password policy requirements.
Response (200):
{
"min_length": 10,
"require_space": true,
"require_grammar_symbol": true,
"grammar_symbols": ". , ! ? ; : ' \" - ( ) [ ] { } @ # $ % ^ & *"
}List available destination mailboxes for the authenticated user.
Response (200):
{
"data": ["user@example.com", "admin@example.com"]
}{
"refresh_token": "abc123..."
}Response (200):
{
"access_token": "eyJ...",
"refresh_token": "xyz789...",
"token_type": "Bearer",
"expires_in": 900
}All alias endpoints require authentication via Authorization: Bearer <token> header.
List aliases forwarding to authenticated user's mailbox.
Query Parameters:
q- Search by local-part (optional)status- Filter by status:active,inactive(optional)page- Page number (default: 1)per_page- Results per page (default: 20, max: 100)sort- Sort by:address,created,modified(default:address)
Response (200):
{
"data": [
{
"id": 1,
"local_part": "alias",
"domain": "example.com",
"address": "alias@example.com",
"destinations": ["user@example.com", "other@example.com"],
"active": true,
"created": "2026-01-01 12:00:00",
"modified": "2026-01-10 14:30:00"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 45,
"total_pages": 3
}
}Create a new alias.
Request:
{
"local_part": "newalias",
"destinations": ["user@example.com", "other@example.com"]
}Constraints:
- Authenticated user's mailbox MUST be in destinations
- Domain is implied from authenticated user
- Local part must not already exist
Response (201):
{
"id": 2,
"local_part": "newalias",
"domain": "example.com",
"address": "newalias@example.com",
"destinations": ["user@example.com", "other@example.com"],
"active": true,
"created": "2026-01-12 10:15:00",
"modified": null
}Update an existing alias.
Request:
{
"local_part": "renamed",
"destinations": ["user@example.com"],
"active": false
}All fields are optional. Provide only fields to update.
Response (200): Same format as create response.
Delete an alias.
Constraints:
- Alias must be inactive (active: false) before deletion
- Returns 409 Conflict if still active
Response (200):
{
"message": "Alias deleted successfully"
}All errors follow this format:
{
"code": "error_code",
"message": "Human-readable error message",
"details": {}
}Common error codes:
invalid_input- Missing or malformed request dataunauthorized- Missing or invalid authenticationinvalid_credentials- Login failednot_found- Resource not foundtls_required- TLS connection requiredrate_limit_exceeded- Too many requests
PostfixMe API is free software licensed under the GNU General Public License v2 or later (GPL-2.0-or-later).
Copyright (c) 2026 William W. Kimball, Jr., MBA, MSIS
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file for full details.