Self-Hosting Guide
Everything you need to deploy LastPost on your own server, configure it for your mail stack, and keep it running.
Download lastpost.php
One-time purchase. Download, deploy, and own it — no subscription, no SaaS dependency.
Server requirements
| Requirement | Minimum | Notes |
|---|---|---|
| PHP | 8.1 | 8.2 or 8.3 recommended. |
PHP extension: imap | Required | For IMAP connections. Package name is usually php-imap on Debian/Ubuntu or php8x-imap on Alpine. |
PHP extension: openssl | Required | For SSL/TLS connections to your mail server. Enabled by default in most PHP builds. |
PHP extension: session | Required | For session-based auth. Enabled by default. |
| Web server | Any | Apache, Nginx, Caddy, LiteSpeed — anything that can run PHP files. |
| HTTPS | Strongly recommended | Your mail credentials pass through LastPost. Run it behind HTTPS. Let's Encrypt makes this free. |
| Database | None | LastPost requires no database. |
To check whether the imap extension is available on your server:
php -m | grep imap
If nothing is returned, install it. On Debian/Ubuntu:
apt install php-imap
systemctl restart php8.x-fpm # adjust version number
Installation
-
Download lastpost.php
Use the download link from your email. The file is a single PHP script — everything is bundled inside it.
-
Edit the configuration block
Open the file in a text editor. The configuration constants are near the top, clearly marked:
// ── Configuration — edit these to match your mail server ────────────── define('LP_IMAP_HOST', 'mail.example.com'); define('LP_IMAP_PORT', 993); define('LP_SMTP_HOST', 'mail.example.com'); define('LP_SMTP_PORT', 465);Set the hostname and ports for your mail server. See the configuration reference for all available constants.
-
Upload the file
Copy
lastpost.phpto your web server. Common locations:/var/www/mail.example.com/lastpost.php— subdomain root/var/www/html/mail/lastpost.php— path under existing site
The file can live anywhere your web server can execute PHP.
-
Set permissions
LastPost needs write access to the directory it lives in for PHP session storage (if using file-based sessions, which is the default). The web server user (
www-dataon Debian,nginxon Fedora) should own the file:chown www-data:www-data /var/www/mail.example.com/lastpost.php -
Navigate to the URL
Open your browser and go to the address where you deployed it. The login screen appears. Enter your IMAP credentials and you're in.
Configuration reference
All configuration is done by editing the define() constants near the top of lastpost.php. No separate config file is needed.
Required
| Constant | Default | Description |
|---|---|---|
LP_IMAP_HOST | 'mail.example.com' | Hostname of your IMAP server. |
LP_IMAP_PORT | 993 | IMAP port. 993 = IMAPS (SSL). Some servers use 143 + STARTTLS. |
LP_SMTP_HOST | 'mail.example.com' | Hostname of your SMTP server. Usually the same as IMAP. |
LP_SMTP_PORT | 465 | SMTP port. 465 = SMTPS (SSL). Some servers use 587 + STARTTLS. |
Optional — maintenance cron
LastPost can sweep expired sessions on a schedule via a simple GET request. This is only needed if you're running a shared instance and want automatic cleanup.
| Constant | Default | Description |
|---|---|---|
LP_CRON_SECRET | '' | Set to a random token to enable the cron endpoint. Leave empty to disable. |
LP_IMAP_USER | '' | IMAP username for cron-triggered maintenance sweeps. |
LP_IMAP_PASS | '' | IMAP password for cron-triggered maintenance sweeps. |
To call the cron endpoint:
GET /api/maintenance/sweep-expired?secret=YOUR_SECRET
Optional — Sieve (vacation auto-reply)
| Constant | Default | Description |
|---|---|---|
LP_SIEVE_HOST | '' | ManageSieve hostname. Leave empty to disable vacation auto-reply UI. |
LP_SIEVE_PORT | 4190 | ManageSieve port. 4190 is the standard. |
Optional — CardDAV (contact sync)
| Constant | Default | Description |
|---|---|---|
LP_CARDDAV_URL | '' | Full CardDAV addressbook URL. Example: https://cloud.example.com/remote.php/dav/addressbooks/users/admin/contacts/ |
LP_CARDDAV_USER | '' | CardDAV username. Defaults to the user's IMAP credentials if empty. |
LP_CARDDAV_PASS | '' | CardDAV password. Defaults to the user's IMAP credentials if empty. |
Architecture overview
Understanding how LastPost is structured helps you deploy it correctly and troubleshoot issues.
Single-file deployment. The entire application ships as one PHP file — lastpost.php. All backend logic, the compiled frontend (HTML, CSS, JS), and bundled locale files are included in that one file. There is nothing else to deploy.
IMAP/SMTP proxy, not a mail server. LastPost does not store or handle email itself. It connects to your existing IMAP server to read mail and to your SMTP server to send mail, acting as a thin authenticated proxy. Your mail stays on your mail server.
No database. There is no database schema to create, no migration to run, and no persistent storage on the server beyond PHP sessions. User preferences (personas, themes, folder bookmarks) are stored in the browser's localStorage.
Session-based authentication. Credentials are provided at login, authenticated against IMAP, and then stored encrypted in a server-side PHP session. The session file is the only server-side state LastPost writes.
Single-page application. The frontend is a vanilla JavaScript SPA served by lastpost.php for all non-API paths. API calls go to /api/* routes handled by the same file, routed by query string (?_api=...) or by URL rewrite if you configure it.
REST API endpoints
All endpoints require authentication (a valid session) except where noted. Prefix every path with /api/. With query-string routing use lastpost.php?_api=PATH.
Authentication
| Method | Path | Auth required | Description |
|---|---|---|---|
GET | auth/status | No | Returns {"authenticated": true|false}. |
POST | auth/login | No | Body: {"user", "pass"}. Authenticates against IMAP and starts a session. |
POST | auth/logout | Yes | Destroys the current session. |
Folders
| Method | Path | Description |
|---|---|---|
GET | folders | List all IMAP folders with unread counts. |
POST | folders | Create a new folder. Body: {"name", "parent?"}. |
POST | folders/rename | Rename a folder. Body: {"old", "new"}. |
DELETE | folders | Delete a folder. Query: ?folder=NAME. |
POST | folders/subscribe | Subscribe to an IMAP folder. |
POST | folders/unsubscribe | Unsubscribe from an IMAP folder. |
GET | folders/unseen | Return per-folder unseen counts. |
Messages
| Method | Path | Description |
|---|---|---|
GET | messages | Paginated message list. Query: ?folder=&page=&limit=. |
GET | messages/{uid} | Fetch a single message with headers, body, and attachment list. |
POST | messages/{uid}/read | Mark a message as read. |
POST | messages/{uid}/flag | Toggle the starred flag on a message. |
DELETE | messages/{uid} | Move to Trash or permanently delete. |
POST | messages/{uid}/move | Move to a different folder. Body: {"to"}. |
POST | messages/{uid}/archive | Move to the Archive folder. |
POST | messages/{uid}/spam | Move to Junk/Spam. |
POST | messages/{uid}/notspam | Move out of Junk/Spam back to INBOX. |
GET | messages/{uid}/raw | Download the raw .eml source. |
GET | messages/{uid}/attachment | Download a single attachment. Query: ?part=. |
GET | messages/{uid}/attachments/zip | Download all attachments as a ZIP. |
Composing and drafts
| Method | Path | Description |
|---|---|---|
POST | send | Send a message via SMTP. Body: {"to", "cc?", "bcc?", "subject", "body", "from?", "replyTo?", "inReplyTo?", "references?"}. |
POST | drafts | Save or update a draft. Returns the draft UID. |
DELETE | drafts/{uid} | Delete a saved draft. |
Search and utilities
| Method | Path | Description |
|---|---|---|
GET | search | Search messages. Query: ?q=&from=&to=&subject=&since=&before=&has_attachment=. |
GET | quota | Return mailbox storage usage and limit in KB. |
GET | export | Export a folder as a .mbox archive. |
POST | cache/clear | Clear server-side message cache for the session. |
POST | unsubscribe | Send an RFC 8058 one-click unsubscribe for a message. |
POST | mute-thread | Mute a conversation thread. |
GET | proxy-image | Proxy an external image through the server to protect privacy. |
GET | config | Return which optional features are enabled (Sieve, CardDAV). |
GET | locale | Return list of available locale codes. |
GET | locale/{lang} | Return the locale JSON for a given language code. |
Optional features
| Method | Path | Requires | Description |
|---|---|---|---|
GET | sieve/vacation | LP_SIEVE_HOST | Return current vacation auto-reply status. |
POST | sieve/vacation | LP_SIEVE_HOST | Push vacation auto-reply to the Sieve server. |
DELETE | sieve/vacation | LP_SIEVE_HOST | Remove the vacation Sieve script. |
GET | carddav/sync | LP_CARDDAV_URL | Fetch all contacts from the configured CardDAV server. |
GET | maintenance/sweep-expired | LP_CRON_SECRET | No session required — secret token in query string. Sweeps expired sessions. |
Apache setup
LastPost works on Apache without any configuration changes. By default it uses query-string routing (lastpost.php?_api=folders). For clean URLs (/api/folders), add an .htaccess file in the same directory as lastpost.php:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ lastpost.php [QSA,L]
This requires AllowOverride All (or at minimum AllowOverride FileInfo) in your Apache virtual host. If you can't use .htaccess, add the rewrite rules directly to your vhost config and omit the .htaccess file.
A minimal virtual host for a dedicated subdomain:
<VirtualHost *:443>
ServerName mail.example.com
DocumentRoot /var/www/mail.example.com
DirectoryIndex lastpost.php
<Directory /var/www/mail.example.com>
AllowOverride All
Require all granted
</Directory>
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/mail.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/mail.example.com/privkey.pem
</VirtualHost>
Nginx setup
A minimal Nginx server block:
server {
listen 443 ssl;
server_name mail.example.com;
root /var/www/mail.example.com;
index lastpost.php;
ssl_certificate /etc/letsencrypt/live/mail.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail.example.com/privkey.pem;
location / {
try_files $uri $uri/ /lastpost.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.x-fpm.sock; # adjust version
}
}
Mail server compatibility
LastPost works with any mail server that supports standard IMAP and SMTP over SSL. Tested and confirmed working:
- Mailu — IMAP port 993, SMTP port 465. No custom configuration needed.
- Mailcow — same ports. Works out of the box.
- iRedMail — IMAP port 993, SMTP port 587 (STARTTLS). Set
LP_SMTP_PORTto 587. - Fastmail — IMAP:
imap.fastmail.com:993, SMTP:smtp.fastmail.com:465. Use an app password. - Gmail — IMAP:
imap.gmail.com:993, SMTP:smtp.gmail.com:465. Requires enabling IMAP in Gmail settings and using an app password (2FA required).
The persona system (send-as identities) works as long as your SMTP server authorizes the From address. On Mailu and Mailcow, aliases you've configured on the server are automatically authorized. On hosted providers like Gmail and Fastmail, you'll need to add send-as addresses through their web interface first.
Vacation auto-reply (Sieve)
If your mail server supports ManageSieve (Mailu and Mailcow both do), LastPost can set up server-side vacation auto-replies from within the app.
To enable it, set LP_SIEVE_HOST to your mail server hostname:
define('LP_SIEVE_HOST', 'mail.example.com');
define('LP_SIEVE_PORT', 4190); // 4190 is the standard; usually correct
Once enabled, a vacation auto-reply panel appears in Settings. You can set the reply message, subject, and how long to wait before re-replying to the same sender.
Contact sync (CardDAV)
LastPost can pull contacts from a CardDAV server to use for autocomplete in the compose To field.
Set LP_CARDDAV_URL to your addressbook URL. The format depends on your CardDAV server — consult its documentation for the correct path. A Nextcloud example:
define('LP_CARDDAV_URL', 'https://nextcloud.example.com/remote.php/dav/addressbooks/users/admin/contacts/');
If your CardDAV credentials are the same as your IMAP credentials, leave LP_CARDDAV_USER and LP_CARDDAV_PASS empty — LastPost will use the login credentials automatically.
Upgrading
Upgrading is the same as installing:
Download the new version
Get the new
lastpost.phpfrom your download link. Each download email contains a fresh link tied to your address.Copy your configuration
Open both the old and new files. Copy the
define()constants from the top of your old file into the same block in the new one.Replace the file on your server
Upload the new
lastpost.php, overwriting the old one. No database migrations, no build steps. Done.
User data (personas, themes, preferences) is stored in localStorage in the browser, so upgrading the server file doesn't touch any of it.
Troubleshooting
Login fails immediately
Confirm your IMAP hostname and port are correct in the configuration constants. Test connectivity from the server directly:
openssl s_client -connect mail.example.com:993
If that fails, the server can't reach your mail server — check firewall rules and DNS.
"PHP imap extension not found"
Install php-imap on your server and restart PHP-FPM. See Server requirements.
Blank page after login
Check your PHP error log. A common cause is the session directory not being writable by the web server user. Confirm /tmp or your configured session.save_path is writable by www-data (or your web server user).
Emails send but recipients see the wrong From name
The From address in your persona must be authorized by your SMTP server. If you send as [email protected] but your SMTP server only permits your primary address, the server may rewrite the From header or reject the message. Add the alias to your mail server's authorized senders list.
STARTTLS instead of SSL (port 587)
Some SMTP servers use STARTTLS on port 587 rather than implicit SSL on 465. Set LP_SMTP_PORT to 587. LastPost detects STARTTLS automatically when port 587 is configured.
Self-signed certificate on your mail server
PHP's imap extension validates SSL certificates by default. If your mail server uses a self-signed cert, IMAP connections will fail. Options: get a real certificate (Let's Encrypt), or pass the CURLOPT_SSL_VERIFYPEER flag — see the note in the configuration block at the top of lastpost.php.
Theme token reference
All 31 theme-controllable token names, for building custom themes:
| Token key | CSS property | Controls |
|---|---|---|
bg | --lapo-bg | Main app background |
bg-surface | --lapo-bg-surface | Cards, panels, sidebar |
bg-raised | --lapo-bg-raised | Elevated surfaces, dropdowns |
bg-input | --lapo-bg-input | Form input backgrounds |
bg-interactive | --lapo-bg-interactive | Hover state backgrounds |
bg-overlay | --lapo-bg-overlay | Modal/overlay backdrop |
accent | --lapo-accent | Primary brand color — buttons, links, highlights |
accent-light | --lapo-accent-light | Lighter accent variant |
accent-dark | --lapo-accent-dark | Darker accent variant |
accent-glow | --lapo-accent-glow | Accent box-shadow glow |
accent-ring | --lapo-accent-ring | Focus ring color |
accent-tint | --lapo-accent-tint | Low-opacity accent fill |
text | --lapo-text | Primary body text |
text-secondary | --lapo-text-secondary | Secondary / supporting text |
text-muted | --lapo-text-muted | Muted / placeholder text |
text-dim | --lapo-text-dim | Even more subdued text |
text-disabled | --lapo-text-disabled | Disabled state text |
border | --lapo-border | Standard border |
border-strong | --lapo-border-strong | Emphasized border |
border-subtle | --lapo-border-subtle | Hairline / very subtle border |
pass | --lapo-pass | Success / positive status color |
pass-bg | --lapo-pass-bg | Success background fill |
pass-border | --lapo-pass-border | Success border |
warn | --lapo-warn | Warning status color |
warn-bg | --lapo-warn-bg | Warning background fill |
warn-border | --lapo-warn-border | Warning border |
fail | --lapo-fail | Error / destructive status color |
fail-bg | --lapo-fail-bg | Error background fill |
fail-border | --lapo-fail-border | Error border |
shadow-sm | --lapo-shadow-sm | Small shadow (cards) |
shadow-md | --lapo-shadow-md | Medium shadow (dropdowns, popovers) |
Include a colors_light block alongside colors to support light mode. The keys are identical — light mode simply swaps to that block when active.