Repository navigation
Setup Maildir
This page sets up ALPS on the server that holds the mail, so that ALPS reads and writes each user's Maildir directly and no IMAP server runs. It covers the passwd file that users sign in against, the maildir path pattern, the user ALPS runs as, what works and what does not with Maildir, and sending through an SMTP submission server. The provider's reference is MAILDIR.md, and Mail-Providers compares it with the imap and multi providers.
- Users sign in with a username and password that ALPS checks against a Dovecot-style passwd file,
/etc/dovecot/userson this page. ALPS reads the file again on every sign-in, so a new or changed entry works without a restart. - Each user's mail is a Maildir at
/var/vmail/<domain>/<user>/Maildir. INBOX is that directory itself. Other folders are Maildir++ dot-directories:Sentis.Sent, andArchive.2024is.Archive.2024. - When ALPS reads a folder, it moves new deliveries from
new/intocur/. Flags are the letters after:2,in the file name (Sread,Ranswered,Fstarred,Tdeleted,Ddraft). Sent copies and drafts are written into the maildir. Renaming a folder renames its subfolders with it. - The message list is ordered by each message's
Dateheader with the default sort (Received Date), and by the delivery time in the file name for a message without a validDate. - What ALPS would keep in IMAP METADATA with an IMAP server goes into
alps_store.jsonat the root of each user's maildir, with mode 0600. That covers the user's ALPS settings, OpenPGP keyring, linked accounts and security keys, and the CalDAV and CardDAV connections the user saves in Settings when those plugins are loaded (Storage-in-IMAP-METADATA). Back it up with the maildir. - Mail goes out through an SMTP submission server. ALPS signs in to it with the username and password the user signed in with.
These come from the provider's limitations.
- New mail is not pushed (
push_new_mail). Pages check for new mail at each user's check-mail interval instead. - There is no storage quota display, no folder sharing, and no server-side threading of the message list.
- Search covers only the current folder, and there is no Search All. Within the folder, a query reads as it does on an IMAP account (SEARCH.md): prefixes such as
from:andsubject:, quoted phrases, several terms together, and the toolbar's Unread and Starred filters, which match the messages' flags. Encoded headers and MIME parts are decoded before they are compared. ALPS opens every message file of the folder for each search. - Sender authentication badges and BIMI logos are not shown.
- Every folder is listed as subscribed, and subscribing or unsubscribing has no effect.
- Tags (
$label1to$label5) and other keywords cannot be stored. A tag change is refused, and no message is changed. - Folder names are shown exactly as they are on disk, with no modified UTF-7 decoding.
-
A Linux host where mail is delivered into Maildirs as one delivery user. This page uses user and group
vmail(uid and gid 5000), maildirs under/var/vmail/<domain>/<user>/Maildir/, and Postfix's virtual delivery agent as the delivery agent. Any other delivery agent needs to write the same layout as the same user. -
An SMTP submission server that accepts each user's ALPS username and password with AUTH PLAIN, and lets the user send as their address. ALPS needs
[smtp] serverwhatever the provider (Mail-Server-Requirements, MAIL_SERVER.md). If that server authenticates through Dovecot's passwd-file, it can read the same/etc/dovecot/users. The scheme table below shows which entries both accept. A submission server on this host shows how to set up Postfix for it. -
The ALPS binary at
/usr/local/bin/alpsand the systemd unitdist/alps.servicefrom the repository (Running-in-Production). -
A TLS reverse proxy on the same host that publishes ALPS at
https://mail.example.comand forwards to127.0.0.1:1323(Setup-Behind-a-Reverse-Proxy). -
One ALPS instance for these maildirs. ALPS serializes changes to
alps_store.jsononly within one process, so do not let two instances serve the same maildirs. With[cluster]enabled, maildir accounts get no recovery codes for security keys (Clustering). -
htpasswd(Debian packageapache2-utils) to make bcrypt password hashes.
Run these as root:
groupadd -g 5000 vmail
useradd -u 5000 -g vmail -d /var/vmail -M -s /usr/sbin/nologin vmail
install -d -o vmail -g vmail -m 0750 /var/vmail
install -d -m 0755 /etc/dovecot /etc/alps[server]
# Reached through the TLS reverse proxy on this host.
addr = "127.0.0.1:1323"
trusted_proxies = ["127.0.0.1/32", "::1/128"]
# Keeps sign-ins across restarts. Generate with: openssl rand -base64 32
login_key = "REPLACE-WITH-YOUR-KEY"
[webauthn]
rpid = "mail.example.com"
origins = ["https://mail.example.com"]
[provider]
type = "maildir"
[provider.maildir]
auth_passwd_file = "/etc/dovecot/users"
path = "/var/vmail/%d/%u/Maildir"
[smtp]
server = "smtps://mail.example.com:465"It holds login_key, so install it readable only by root and the vmail group:
install -o root -g vmail -m 0640 config.toml /etc/alps/config.tomlWhat each part does:
-
auth_passwd_fileis required. Without it ALPS stops at startup withno password file specified in config file for maildir provider ([provider.maildir] auth_passwd_file). Without the[provider.maildir]section it stops withmissing TOML section [provider.maildir]. ALPS does not check at startup that the file exists or can be read. That shows up at the first sign-in (see Troubleshooting). -
pathis a pattern for each user's own maildir: the directory that holdscur,new,tmp, the dot-folders andalps_store.json. ALPS adds nothing to it. In the pattern,%uis the local part of the login,%dis the domain and%nis the whole login. For a login without@,%uand%nare the whole login and%dis empty. Use%utogether with%d, or use%n, so that every account gets its own directory. A pattern without placeholders makes all accounts share one maildir and one settings file. Whenpathis set, some logins cannot sign in: one with an empty local part, and one whose local part or domain contains/or\or is.or... -
Without
path, each user's maildir is<home>/Maildir, where<home>is field 6 of their passwd entry. Every entry then needs a home field. With the entries below, both forms give the same directory. -
[smtp] serveris required at startup, with themaildirprovider too. Without it ALPS stops withno SMTP server specified in config file ([smtp] server).smtps://uses implicit TLS, on port 465 when the URL names none.smtp://requires STARTTLS, on port 587 when the URL names none. Certificates are checked against the host's system roots. Config-Providers-and-SMTP lists the[smtp]keys. -
trusted_proxieslists only your own proxy, so that the rate limits and logs see each client's real address (Config-Server). -
login_keyis optional. Without it no session survives a restart and accounts cannot be linked (Sessions-and-Sign-in). Keep it secret. -
[webauthn]names the host users reach ALPS at, so that security keys work. Withoutrpid, ALPS logs a warning at startup (Config-Rate-Limiting-and-WebAuthn). -
[provider] timeout_sechas no effect on themaildirprovider. - With no
[plugin.*]section, the plugins that need nothing configured load:baseandgpg(OpenPGP). - ALPS warns at startup about every key it does not know, for example
Unknown configuration key: provider.maildir.paht (ignored), and keeps that key's default.
# <user>:<password>:<uid>:<gid>:<gecos>:<home>:<shell>:<extra fields>
john@example.com:{CRYPT}$2y$12$puyzuBw7zxJnwO7Lm.rjVeWms/SWepzFi8B8oawdVD8LKMLmcyPiO:5000:5000::/var/vmail/example.com/john::
The hash above is of the password example-password. Make your own for every account (see Adding an account).
install -o root -g vmail -m 0640 users /etc/dovecot/usersALPS only reads this file, so vmail needs read access and nothing more. Of each line ALPS uses the user (field 1), the password (field 2) and the home directory (field 6). It ignores the uid, the gid and any extra fields, and does every file operation as its own user. Blank lines and lines starting with # are skipped. The first field must match the login exactly as the user types it, including letter case: John@example.com does not sign in as john@example.com. Only the first line whose user matches is tried. The full format is in MAILDIR.md.
The password field starts with a scheme tag. Tags are case-insensitive. A field without a tag is compared as plain text, so give every entry its tag. These entries were tried in the lab, against ALPS and against Dovecot 2.4.5's passwd-file:
| Password field | ALPS | Dovecot 2.4.5 |
|---|---|---|
{CRYPT} + bcrypt ($2y$; $2a$, $2b$, $2x$ also accepted) |
Yes. Recommended. | Yes |
{SHA256} or {SHA512} + hex digest (upper or lower case) |
Yes | Yes |
{MD5} + hex digest |
Yes | No: refused as a weak scheme. Dovecot's name for this digest is {PLAIN-MD5}, a tag ALPS does not accept. |
{PLAIN} + the password (cannot contain :) |
Yes | Yes |
{CRYPT} + SHA512-crypt ($6$) |
No | Not tried |
{BLF-CRYPT} + bcrypt |
No (works once the tag is {CRYPT}) |
Not tried |
{SHA256} + base64 digest (what doveadm pw -s SHA256 prints) |
No | Not tried |
{SHA256.HEX} |
No | Not tried |
ALPS also refuses $5$, $1$ and DES hashes under {CRYPT}, the {SHA256-CRYPT} and {SHA512-CRYPT} tags, and salted and other schemes such as {SSHA*}, {SMD5}, {SHA}, {PLAIN-MD5}, {PBKDF2}, {ARGON2*} and {CRAM-MD5} (supported schemes). Unsalted digests are weak, so use bcrypt. An entry in a scheme ALPS refuses can never sign in. The user sees the same message as for a wrong password, and ALPS logs a warning that names the user and the scheme, such as alps/provider/maildir: warning: user "john@example.com" has unsupported password scheme "{CRYPT}" for a $6$ hash. The warning leaves out the hash and the password.
The shipped unit runs ALPS as user alps, with the whole file system read-only (ProtectSystem=strict). ALPS creates folders and their tmp, new and cur with mode 0700, so it has to run as the user that owns the mail. It also needs write access to the maildirs. Create /etc/systemd/system/alps.service.d/maildir.conf:
[Service]
User=vmail
Group=vmail
ReadWritePaths=/var/vmailinstall -m 0644 dist/alps.service /etc/systemd/system/alps.service
install -d -m 0755 /etc/systemd/system/alps.service.d
install -m 0644 maildir.conf /etc/systemd/system/alps.service.d/maildir.confProtectHome=true in the unit hides /home. For maildirs under /home, set ProtectHome=false in the same drop-in (OPERATIONS.md).
This page used Postfix's virtual delivery agent, writing as uid and gid 5000 into the same paths:
postconf -e 'virtual_mailbox_domains = example.com' \
'virtual_mailbox_base = /var/vmail' \
'virtual_mailbox_maps = texthash:/etc/postfix/vmailbox' \
'virtual_uid_maps = static:5000' \
'virtual_gid_maps = static:5000'
systemctl restart postfix/etc/postfix/vmailbox holds one line per account. The trailing / makes Postfix deliver in Maildir format:
john@example.com example.com/john/Maildir/
Keep example.com out of Postfix's mydestination. On its first delivery, Postfix creates the user's maildir as vmail with mode 0700.
ALPS needs a submission server that takes the same usernames and passwords; the lab used one on another host. With Postfix on this host, the submission service from Setup-Dovecot-and-Postfix works with this page's accounts, with these differences:
- Take from that page the
submissionentry inmaster.cf,smtpd_sender_login_mapswith/etc/postfix/sender_login, and themyhostnameand TLS lines ofmain.cf. - Postfix checks the passwords through Dovecot's authentication service. Install
dovecot-coreonly, withoutdovecot-imapd,dovecot-lmtpdordovecot-managesieved. In Dovecot's configuration, name this page's/etc/dovecot/usersas thepasswd-filepassdb, and add theservice authlistener for Postfix from that page. Dovecot's authentication runs as thedovecotuser, which must be able to read the file as well asvmail; for example, adddovecotto thevmailgroup (usermod -aG vmail dovecot) and restart Dovecot. - Leave out the rest of that page: the IMAP, LMTP and Sieve parts, and
virtual_transport = lmtp:unix:private/dovecot-lmtp, which would replace the virtual delivery agent this page uses. - In ALPS, set
[smtp] server = "smtp://mail.example.com:587".smtp://always upgrades with STARTTLS, which that submission entry requires.
This combination was not part of the lab run.
Run these as root in bash. They add the passwd entry and the Postfix line, and create the maildir with its usual folders as vmail:
read -rs -p 'Password: ' PW; echo
HASH=$(printf '%s\n' "$PW" | htpasswd -niBC 12 "" | tr -d ':\n'); unset PW
echo "john@example.com:{CRYPT}$HASH:5000:5000::/var/vmail/example.com/john::" >> /etc/dovecot/users
echo "john@example.com example.com/john/Maildir/" >> /etc/postfix/vmailbox
postfix reload
runuser -u vmail -- sh -c 'umask 077; d=/var/vmail/example.com/john/Maildir
for f in "" .Drafts .Sent .Trash .Junk .Archive; do mkdir -p "$d/$f/cur" "$d/$f/new" "$d/$f/tmp"; done'Then give the account the same username and password on the submission server.
The runuser line creates the maildir and the usual folders, so that they show from the first sign-in. Without it, ALPS creates the maildir, mode 0700, the first time it saves the user's settings, and creates Sent or Drafts the first time it stores a sent copy or a draft. It files sent copies only into a folder named Sent, and drafts into one named Drafts, or Draft when there is no Drafts, in any letter case. A folder whose name only contains the word, such as Presentations, is never used.
systemctl daemon-reload
systemctl enable --now alps
systemctl status alps
journalctl -u alps -n 20systemctl status alps should show active (running) and list the drop-in under Drop-In:. The journal should end with the line "msg":"Starting server on 127.0.0.1:1323", and should have no rpid is unset warning. A configuration error makes ALPS exit, and systemd starts it again every 5 seconds. The journal then shows the same startup error over and over.
The checks below run on the server. They sign in to ALPS directly at 127.0.0.1:1323, and send the Origin header that ALPS's request check expects for that address.
-
Deliver a message and see it land as
vmail:printf 'From: postmaster@example.com\nTo: john@example.com\nSubject: First message\n\nHello.\n' \ | /usr/sbin/sendmail -f postmaster@example.com john@example.com ls -l /var/vmail/example.com/john/Maildir/new
Postfix delivers in the background, so run
lsagain ifnew/is still empty. The file belongs tovmailand has mode 0600. -
Sign in and list the inbox:
A=http://127.0.0.1:1323; J=$(mktemp) curl -s -c "$J" -b "$J" -H "Origin: $A" -H 'Content-Type: application/json' \ -d '{"username":"john@example.com","password":"the password"}' "$A/session" curl -s -b "$J" "$A/mailboxes/INBOX/status" curl -s -b "$J" "$A/mailboxes/INBOX" | grep -o '"Subject":"[^"]*"'
Expect
{"ok":true}, then{"total":1,"unseen":1,...}, then"Subject":"First message". The file has now moved fromnew/tocur/, with:2,added to its name. -
Save a setting:
curl -s -b "$J" -X PUT -H "Origin: $A" -H 'Content-Type: application/json' \ -d '{"signature":"John"}' "$A/settings" ls -l /var/vmail/example.com/john/Maildir/alps_store.json
Expect
{"ok":true}and a file with-rw------- vmail vmail. -
Send to an address you can read:
curl -s -b "$J" -X POST -H "Origin: $A" -F to=you@example.org \ -F subject='Hello from ALPS' -F text='Sent through the submission server.' "$A/messages" ls /var/vmail/example.com/john/Maildir/.Sent/cur rm -f "$J"
Expect
{"ok":true}, the message at the recipient, and a copy in.Sent/curwhose name ends in:2,S. -
In a browser, open
https://mail.example.com, sign in, open the message in the Inbox, then click Compose, write a message and send it. The copy appears in Sent.
These are the cases met while building this setup. "The service could not sign you in" below is the login form's message for an answer of 503. In the journal, those sign-in failures appear as "msg":"sign-in failed" with "reason":"error", and the cause in the "error" field.
| What you see | Cause and fix |
|---|---|
systemctl status alps shows status=217/USER. The journal says Failed at step USER spawning /usr/local/bin/alps. |
The unit runs as alps and that user does not exist. Install the drop-in with User=vmail. |
"The service could not sign you in just now." Journal: failed to open passwd file: open /etc/dovecot/users: permission denied
|
ALPS's user cannot read the passwd file. Make it root:vmail 0640 and run ALPS as vmail. |
The same message. Journal: failed to check whether john@example.com signs in with 2FA: open /var/vmail/example.com/john/Maildir: permission denied
|
ALPS runs as a user that cannot open the maildir. Run it as the user that owns the mail. |
"Login failed. Please check your credentials." for a password you know is right. Journal: "reason":"bad credentials"
|
When the journal also has has unsupported password scheme for that user, the entry uses a scheme ALPS refuses (see the table). Otherwise the login is typed in a different case from field 1, or there is no entry. |
Saving settings fails with "An internal error occurred". Journal: failed to save settings: openat alps_store.json.tmp: read-only file system
|
ReadWritePaths=/var/vmail is missing. In the same state, a send goes out but its copy cannot be stored: the composer says "Message sent, but no copy could be saved in Sent.", and the journal has Sent message cleanup: filing=… with the same read-only file system. |
"The service could not sign you in just now." Journal: has no home directory in passwd file and no path pattern is configured
|
path is unset and the entry has no field 6. Add the home field, or set path. |
An account that signed in before now gets "The service could not sign you in just now." The journal has an ERROR line that ends in a JSON error, such as invalid character 'b' looking for beginning of object key string. |
alps_store.json is no longer valid JSON, for example after a hand edit. Restore it from a backup, or fix the JSON. |
Sending fails with "Failed to send message: Forbidden" (the request log shows POST /messages with status 403). |
The submission server refused the user's username and password. Create the account there with the same password. |
ALPS exits at start with no password file specified ..., missing TOML section [provider.maildir] or no SMTP server specified .... |
Add the key or section named. |
ALPS main a5443047, on 2026-11-12. Debian 13.7 (arm64) in a container, with systemd 257.13-1deb13u1 as PID 1 running the shipped unit and the drop-in above. Postfix 3.10.13 handled virtual delivery, apache2-utils 2.4.71-1deb13u1 made the hashes (htpasswd), and curl 8.14.1 ran the checks. The container ran under Docker Desktop 29.9.2 on macOS 26.4. Dovecot 2.4.5 (official container image) ran doveadm pw and the passwd-file check. The submission server was sora's, from the ALPS browser-test lab. The browser check used Chromium from Playwright 1.62.0. The lab used the domain example.test, and sent through that submission server over smtp+insecure:// on the lab network. Apart from those values, its files match the ones on this page. The page was then followed again, step by step, in a fresh container with the same package versions, with the browser check made through an nginx proxy in front of ALPS.
Updated from the code at main 92ec0e11 without a rerun: the CalDAV and CardDAV connections kept in alps_store.json, and the startup warning for unknown keys. The configuration file above was started once with a 92ec0e11 build on macOS, to check that ALPS knows every key and loads base and gpg.
These pages describe ALPS as it is on the main branch; the reference documents live in the repository's docs/ directory.
Architecture
- Architecture Overview
- Mail Server Requirements
- Sessions and Sign-in
- Storage in IMAP METADATA
- Mail Providers
- Plugins
- Features
- Security Features
- Clustering
Tests
Configuration
- Configuration Overview
- Server
- Providers and SMTP
- TLS
- Cluster
- Rate Limiting and WebAuthn
- Logging and Cache
- Plugins
- Branding
Common Setups