Version 3.4.0 · Single-file PHP · AES-256-GCM · Federated · Self-hosted
- Project Overview
- Security and Privacy Benefits
- Features and Advantages
- Installation Instructions
- Usage Guide
- php.ini Configuration
- File Structure
- License
XMail is a self-hosted, federated, end-to-end encrypted mail system delivered as a single PHP file (xmail.php). It is designed for developers, privacy advocates, and organisations who require full control over their messaging infrastructure without relying on third-party mail providers.
Unlike traditional SMTP-based email, XMail uses a custom federation protocol over HTTPS with a cryptographic handshake to verify sender identity before delivery. All message bodies and attachments are encrypted with AES-256-GCM using a per-message password chosen by the sender — the server stores only ciphertext and never has access to the plaintext content.
XMail requires no external dependencies, no package manager, and no database server. Drop a single file onto any PHP-capable web host and you have a fully functional, secure, private mail node that can communicate with every other XMail instance on the internet.
XMail was designed from the ground up with a defence-in-depth security posture. Every layer of the stack — from transport to storage — incorporates hardening measures.
| Layer | Algorithm | Detail |
|---|---|---|
| Key derivation | PBKDF2-SHA256 | 100,000 iterations, 16-byte random salt per message |
| Message encryption | AES-256-GCM | 12-byte IV, 16-byte authentication tag |
| Attachment encryption | AES-256-GCM | Same key as message body; encrypted before disk write |
| Stored filenames | bin2hex(random_bytes(16)) |
No correlation between original and stored names |
The encryption key is never stored on the server. It is derived at runtime from the password supplied by the user in the browser. Even direct database access reveals nothing but opaque ciphertext.
- HSTS (
Strict-Transport-Security: max-age=31536000; includeSubDomains) is sent automatically when the application is served over HTTPS. - Federation requests are made over HTTPS first, with HTTP as a fallback, and never to private or reserved IP ranges (SSRF protection).
- Redirect validation on all outbound federation fetches — redirects to private hosts are blocked, and
follow_locationis disabled at the stream-context level.
Every response carries the following headers:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-{random}'; script-src 'self' 'nonce-{random}'
Strict-Transport-Security: max-age=31536000; includeSubDomains (HTTPS only)
The CSP nonce (random_bytes(18)) is regenerated on every request, preventing inline script injection.
- Sessions use Secure, HttpOnly, and SameSite=Lax cookie attributes.
- CSRF tokens (48 hex characters,
hash_equalscomparison) protect every state-changing form — including registration, settings, and all admin actions. - Login rate limiting: after 10 failed attempts from a single IP within a 15-minute window, the IP is blocked for a further 15 minutes. Only
REMOTE_ADDRis used —X-Forwarded-Forand similar headers are ignored to prevent IP spoofing. - Passwords are hashed with bcrypt via
password_hash()/password_verify().
- Sender verification handshake: when a message arrives at
?api=receive, the receiving server performs a back-channelPOSTto?api=verify_sendon the sender's server. The sender's server confirms the token, from-address, and intended recipient before delivery proceeds. This prevents local-sender spoofing — there is no bypass even for messages arriving from the same server. - Token binding: outbound tokens are bound to the
(token, to_user)pair, preventing token replay across different recipients. - Token TTL: tokens expire after 3600 seconds and are deleted on expiry check or successful use.
- Host Header Injection:
BASE_URLis computed once at boot from a sanitisedHTTP_HOSTvalue and stored as a constant, preventing host header injection throughout the application.
attachments/.htaccessis auto-generated withDeny from all,Options -Indexes, and a PHP execution block so uploaded files can never be served directly or executed.- A root-level
.htaccessis auto-generated to deny direct access to.db,.sqlite, and.sqlite3files. - CRLF injection in
Content-Dispositionheaders is prevented by using thefilename*RFC 5987 parameter exclusively. - Attachment MIME types are validated against an explicit allowlist using
mime_content_type()on the actual file content, not the client-supplied type.
- Single-file deployment — the entire application is contained in
xmail.php. No framework, no Composer, no npm. - End-to-end encryption — AES-256-GCM with PBKDF2 key derivation. The server holds only ciphertext.
- Federated architecture — send encrypted messages to users on any other XMail instance using the
user@host/xmail.phpaddress format. - Encrypted attachments — files up to 10 MB are encrypted with the message key before being written to disk.
- EML export (v3.4.0) — decrypted messages (body + attachments) can be exported as standards-compliant RFC 2822
.emlfiles and opened in Thunderbird, Apple Mail, Outlook, or any compliant mail client. - Threaded conversations — replies are grouped by thread UID, preserving conversation context.
- Draft support — compose and save drafts; resume at any time.
- CC and BCC — full CC/BCC support on both local and federated sends.
- Folder management — Inbox, Sent, Drafts, and Trash with bulk-action support.
- Unread counts — per-folder unread badge in both sidebar and bottom navigation.
- Responsive UI — works on desktop and mobile; collapsible sidebar and a bottom navigation bar for small screens.
- Admin panel — user management (create, delete, toggle admin), registration gating (open/closed), and system statistics.
- SQLite database — zero-configuration, file-based persistence with WAL mode and foreign key enforcement.
- No external dependencies — requires only
pdo_sqliteandopensslPHP extensions.
| Requirement | Minimum |
|---|---|
| PHP | 8.0 or later |
| PHP extensions | pdo_sqlite, openssl |
| Web server | Apache, Nginx, Caddy, or any server capable of running PHP |
| HTTPS | Strongly recommended for production |
| Writable directory | The directory containing xmail.php must be writable by the web server process |
Clone the repository or download xmail.php directly:
git clone https://github.com/xsukax/XMail.git
cd XMailOr download the single file:
curl -O https://raw.githubusercontent.com/xsukax/XMail/main/xmail.phpCopy xmail.php to your web server's document root or a subdirectory:
cp xmail.php /var/www/html/xmail.phpEnsure the directory is writable by the web server so XMail can create xmail.db and the attachments/ directory automatically:
chown www-data:www-data /var/www/html/
chmod 750 /var/www/html/php -m | grep -E 'pdo_sqlite|openssl'Both pdo_sqlite and openssl must appear in the output. If either is missing, see php.ini Configuration.
<VirtualHost *:443>
ServerName mail.yourdomain.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/ssl/certs/yourdomain.crt
SSLCertificateKeyFile /etc/ssl/private/yourdomain.key
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>For Nginx:
server {
listen 443 ssl;
server_name mail.yourdomain.com;
ssl_certificate /etc/ssl/certs/yourdomain.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
root /var/www/html;
index xmail.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Block direct access to the database and attachments
location ~* \.(db|sqlite|sqlite3)$ { deny all; }
location /attachments/ { deny all; }
}Navigate to https://mail.yourdomain.com/xmail.php. On first load, XMail initialises the database, creates the attachments/ directory, and redirects to the first-run setup screen.
On the very first visit XMail detects the absence of an admin account and presents a setup form. Fill in a username and strong password to create the initial administrator account. Subsequent registrations are controlled by the Admin Panel (open or closed).
flowchart TD
A([Browser: GET /xmail.php]) --> B{Admin account\nexists?}
B -- No --> C[First-Run Setup Form]
C --> D[Create Admin User]
D --> E[Redirect to Inbox]
B -- Yes --> F{Logged in?}
F -- No --> G[Login Form]
G --> H{Credentials valid\n& not rate-limited?}
H -- Yes --> E
H -- No --> I[Record failed attempt\nShow error]
F -- Yes --> E
- Click ✉️ Compose in the navigation.
- Enter the recipient address in
user@host/pathformat (e.g.alice@mail.example.com/xmail.php). - Optionally add CC and BCC recipients.
- Enter a subject and message body.
- Set a message password — this is the encryption key. Both the sender and recipient need this password to read the message.
- Attach files if needed (up to 10 MB per attachment; JPEG, PNG, PDF, ZIP, DOCX, XLSX, and plain text are accepted).
- Click Send or Save Draft.
Important: The message password is never transmitted to the server. Choose a secure password and communicate it to your recipient through a separate channel.
flowchart LR
A([Open Inbox]) --> B[Select message]
B --> C{Thread key\nin session?}
C -- Yes --> D[Decrypt & display body]
C -- No --> E[Show 🔒 locked prompt]
E --> F[Enter message password]
F --> G{Decryption\nsuccessful?}
G -- Yes --> D
G -- No --> H[Show decryption error]
D --> I{Attachments?}
I -- Yes --> J[Decrypt & download attachment]
I -- No --> K([Done])
J --> K
Once a thread is successfully decrypted, the session retains the thread key so you do not need to re-enter the password while navigating within the same session.
XMail uses a three-step federation protocol to deliver messages cross-instance while verifying sender identity:
sequenceDiagram
participant Sender as Sender's Browser
participant SenderServer as Sender's XMail Instance
participant ReceiverServer as Receiver's XMail Instance
Sender->>SenderServer: POST compose (to=alice@remote/xmail.php)
SenderServer->>SenderServer: Generate verify_token, store in outbound_tokens
SenderServer->>SenderServer: Encrypt message body & attachments
SenderServer->>ReceiverServer: POST ?api=receive {body_enc, verify_token, ...}
ReceiverServer->>SenderServer: POST ?api=verify_send {verify_token, from_addr, to_user}
SenderServer-->>ReceiverServer: {ok: true, from_addr: "..."}
ReceiverServer->>ReceiverServer: Validate handshake & store ciphertext
ReceiverServer-->>SenderServer: {ok: true}
SenderServer-->>Sender: Message delivered / show confirmation
Cross-instance addresses use the format:
username@hostname/xmail.php
For example: bob@secure.example.org/xmail.php
After successfully decrypting a message thread, an Export .eml button appears next to each message. Clicking it downloads a fully standards-compliant RFC 2822 / MIME .eml file containing the decrypted body and all decrypted attachments. The exported file can be opened in:
- Mozilla Thunderbird
- Apple Mail
- Microsoft Outlook
- GNOME Evolution
- Any RFC 2822-compliant mail client
The export endpoint (?dl_eml=<mail_id>) requires an active session with the thread key present — it is not accessible without prior decryption in the UI.
Access the admin panel via ⚙️ More → Admin (visible only to admin-role users).
| Capability | Description |
|---|---|
| View statistics | Total users, total messages, registration status |
| Toggle registration | Open or close new user registration |
| Grant / revoke admin | Promote or demote any other user |
| Delete user | Permanently removes the user and all their messages |
graph TD
subgraph Instance A ["Instance A (mail.alice.net/xmail.php)"]
UA[User: alice]
DBA[(xmail.db)]
ATTA[attachments/]
end
subgraph Instance B ["Instance B (mail.bob.org/xmail.php)"]
UB[User: bob]
DBB[(xmail.db)]
ATTB[attachments/]
end
subgraph Instance C ["Instance C (mail.carol.io/xmail.php)"]
UC[User: carol]
DBC[(xmail.db)]
ATTC[attachments/]
end
UA -- "HTTPS POST ?api=receive" --> UB
UA -- "HTTPS POST ?api=receive" --> UC
UB -- "HTTPS POST ?api=receive" --> UA
UB -- "Back-channel verify_send" --> UA
UA -- "Back-channel verify_send" --> UB
Each instance is fully autonomous. There is no central registry, directory server, or shared infrastructure.
stateDiagram-v2
[*] --> FirstRun : No admin account
FirstRun --> Inbox : Admin created
[*] --> Login : Admin exists
Login --> Inbox : Authenticated
Inbox --> Compose : New message
Inbox --> Thread : Open message
Thread --> Locked : Key not in session
Locked --> Thread : Enter password
Thread --> EMLExport : Export .eml (decrypted only)
Thread --> Reply : Reply to thread
Reply --> Compose
Compose --> Sent : Delivered
Compose --> Drafts : Save draft
Drafts --> Compose : Resume draft
Sent --> [*]
Inbox --> Trash : Move to trash
Trash --> Inbox : Restore
Trash --> [*] : Delete permanently
XMail requires two PHP extensions: pdo_sqlite and openssl. On most shared and managed hosts these are enabled by default. If they are absent, enable them in your php.ini:
; Required extensions
extension=pdo_sqlite
extension=openssl
; Recommended settings for file upload support
file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
; Recommended session security
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
; Recommended for production
display_errors = Off
log_errors = On
error_log = /var/log/php/error.logAfter editing php.ini, restart your PHP-FPM or web server process:
# PHP-FPM
sudo systemctl restart php8.2-fpm
# Apache with mod_php
sudo systemctl restart apache2Verify the configuration is active:
php -r "echo extension_loaded('pdo_sqlite') ? 'pdo_sqlite OK' : 'pdo_sqlite MISSING'; echo PHP_EOL;"
php -r "echo extension_loaded('openssl') ? 'openssl OK' : 'openssl MISSING'; echo PHP_EOL;"After first launch the following files and directories are created automatically:
XMail/
├── xmail.php # The entire application (source of truth)
├── xmail.db # SQLite database (auto-created on first run)
├── .htaccess # Blocks direct access to .db / .sqlite files (auto-created)
└── attachments/ # Encrypted attachment storage (auto-created)
└── .htaccess # Blocks direct HTTP access to attachment files (auto-created)
Security note: Ensure
xmail.dband theattachments/directory are not publicly accessible. The auto-generated.htaccessfiles handle this on Apache. On Nginx, apply thedeny alldirectives shown in the Installation Instructions.
This project is licensed under the GNU General Public License v3.0 — see https://www.gnu.org/licenses/gpl-3.0 for the full license text.