Repository navigation
Features
This page covers the ALPS features that an operator turns on, sizes or plans for, apart from the plugins (contacts, calendars, filters, OpenPGP and password change, see Plugins). The features are new-mail push, change numbers (CONDSTORE), message previews, search, attachments, languages and right-to-left layout, BIMI sender logos, and branding. For each one, the page says what users get, what to set, what the mail server must offer, and how to check that it works. The keys are described in full in CONFIGURATION.md. ALPS reads its configuration only at startup, so restart it after a change: SIGHUP only reopens a log file.
| Feature | What the operator sets | What the mail server must offer | Default |
|---|---|---|---|
| New-mail push |
[server] push_new_mail, push_subscribed_folders
|
IDLE, and NOTIFY for folders other than the Inbox | Off |
| Change numbers | [server] condstore |
CONDSTORE | Off |
| Message previews | Nothing | PREVIEW (RFC 8970) | On wherever the server lists PREVIEW |
| Search | Nothing | IMAP SEARCH; MULTISEARCH for Search All; THREAD and SORT for how results are listed | On |
| Attachments |
[server] max_attachment_mib, max_session_attachment_mib, max_global_attachment_mib, temp_dir
|
Message size limits large enough for the encoded files; ALPS reads APPENDLIMIT and SIZE where the servers list them |
32, 128 and 1024 MiB |
| Languages and right-to-left | Only the default first day of the week, [plugin.caldav] options.week_start
|
Nothing | Each user's browser language |
| BIMI sender logos | [provider.imap] authserv_ids |
Authentication-Results fields written by the receiving server |
On, and each user can turn sender avatars off |
| Branding |
[branding] default_theme, logo
|
Nothing | The Alps theme and the edelweiss logo |
Mail Server Requirements lists every IMAP capability ALPS uses and what it does without each one.
Without push, the open mail pages check for new mail at the user's interval: Settings > General > Check mail interval, every 1, 5 (the default), 15 or 30 minutes. With push on, the session waits for the mail server's news on its background connection (IMAP IDLE) and passes it to the page at once over an event stream (GET /events, Server-Sent Events). The page then checks straight away instead of at its next interval. The interval checks keep running either way, and push only makes the check after a change immediate.
Push reports messages that arrive in the Inbox or leave it. Flag changes, and the other folders, are picked up by the interval checks. With push_subscribed_folders on as well, and a mail server with NOTIFY (RFC 5465), the same news comes for every subscribed folder. The interval checks then run a third as often, every 15 minutes at the default interval of 5.
[server]
push_new_mail = true
# Optional: news of the subscribed folders too, on a server with NOTIFY
push_subscribed_folders = truePush needs all of the following:
-
background_connectionleft on, which is the default. With it off, ALPS logspush_new_mail needs background_connection, which is off: new mail is not pushed,GET /settingsreports"PushNewMail": false, and/eventsanswers 404. -
A mail server with IDLE. If the server has none, the stream tells the page so and ends, and the page goes on checking at its interval. Without NOTIFY, or if the server refuses it, only the Inbox is pushed.
push_subscribed_foldersalone, withoutpush_new_mail, pushes nothing, and ALPS logspush_subscribed_folders needs push_new_mail, which is off: no news of the folders is pushed. -
HTTPS, or
localhost. The page uses the browser'snavigator.locksso that one tab holds the stream for all the tabs of that browser, and browsers offer it only in a secure context. When ALPS is served over plain HTTP on any other host, no tab opens a stream. -
A reverse proxy that passes the stream through unbuffered and keeps it open. ALPS answers
/eventswithCache-Control: no-cacheandX-Accel-Buffering: no(nginx honours the latter), and writes a heartbeat line every 20 seconds. The proxy's read or idle timeout (nginxproxy_read_timeout, HAProxytimeout server) must be longer than 20 seconds. Other proxies may need buffering turned off for/events. See Setup Behind a Reverse Proxy. - In a cluster, routing to the session's node. The stream must reach the node that holds the session, as every other request of that session must (see Clustering).
What push costs:
- Two IMAP connections for every session with a mail page open, for as long as the page is open. Without push, a session's background connection closes after a minute unused, so a session mostly holds one connection. All connections come from ALPS's own address, so check the mail server's per-user and per-address connection limits. For example, a user with mail open in two browsers holds four connections. When the server refuses a session its second connection, that session gets no push and keeps checking at its interval.
-
Work on the mail server. The server watches the Inbox of every open page, and with
push_subscribed_foldersit also watches every subscribed folder of every open page. CONFIGURATION.md, Pushing new mail puts numbers on this for sora.
ALPS begins the IDLE again every 25 minutes. A failed or refused IDLE never signs anyone out. ALPS tries it again after a minute, and after each further failure in a row it waits twice as long, up to about half an hour.
To check, open a mail page over HTTPS and look at the browser's developer tools. One tab has a pending request to /events. Its first event is watching, with the data INBOX, or SUBSCRIBED when the subscribed folders are pushed too. An inbox event follows each arrival. To check that the proxy does not buffer the stream, sign in with a test account that has no two-factor sign-in, and read the stream with curl:
curl -s -c jar -H 'Content-Type: application/json' -H 'Origin: https://webmail.example.com' \
-d '{"username":"test@example.com","password":"..."}' https://webmail.example.com/session
curl -N -b jar https://webmail.example.com/eventsThe event: watching lines should come at once, and a : still here line every 20 seconds. Lines that arrive late or in bursts mean that something between curl and ALPS is buffering.
How soon an event follows a change depends on the mail server. sora looks for changes every 15 seconds for each connection in IDLE or NOTIFY. In the test lab against sora, a message delivered to the Inbox and a message copied into a subscribed folder each showed up within 15 seconds, as event: inbox and event: folder.
The Vite dev server forwards /events like the other API paths, so push can also be tried through it, on http://localhost:5173.
A session caches the pages of each folder it lists, and keeps them while the folder's counts stay the same. Without condstore, a flag set in another client that leaves the counts alone, such as a star, shows only when the cached page expires ([cache] ttl_minutes, default 10). A change that moves the unread count makes ALPS read every page of the folder again in full.
With condstore on, each mail connection asks the server to number the changes to its messages (IMAP CONDSTORE, RFC 7162). The STATUS checks then carry the folder's latest change number. When the number moves but the messages are the same, ALPS reads only the flags that changed, with one UID FETCH ... (FLAGS) (CHANGEDSINCE n), and lays them over the cached page. A star or a read in another client then shows at the next check.
[server]
condstore = true- The server must list CONDSTORE itself. A server that lists QRESYNC counts too, but one that lists only
IMAP4rev2does not, since RFC 9051 does not include CONDSTORE. ALPS uses a server without it as ifcondstorewere off. - ALPS turns CONDSTORE on with
ENABLE CONDSTOREstraight after sign-in, where the server lists ENABLE (orIMAP4rev2, which includes it). On a server without ENABLE, the firstSTATUSthat asks for the change number turns it on. QRESYNC is never turned on, even where the server lists it. - The cost is one round trip as each connection signs in. A change made in ALPS itself also moves the number, so the next check reads the page again with that one
FETCH. - It complements push: push makes arrivals immediate, and CONDSTORE makes flag changes cheap to pick up at each check.
To check, read a folder's status after signing in. With CONDSTORE in use, the answer carries highestmodseq as a decimal string:
curl -b jar https://webmail.example.com/mailboxes/INBOX/status{"total":6,"unseen":6,"uidnext":7,"uidvalidity":1794476250,"highestmodseq":"19"}Without it, highestmodseq is absent. In the test lab, a star set over IMAP from another client showed in the next listing with condstore = true. With the key off, it did not show in the next listing, which came from the cached page.
Details: CONFIGURATION.md, Numbered changes.
When the IMAP server lists PREVIEW (RFC 8970), the message list shows a line of each message's text under its subject. No ALPS key turns this on or off. ALPS uses PREVIEW whenever the server lists it after sign-in, and shows no preview line when it does not.
- A listing asks for
PREVIEW (LAZY), so the server answers only the previews it already has at hand, and the listing is not slowed down. - The page then asks for the missing previews in the background, a few messages per request (
GET /mailboxes/{mbox}/previews?uids=..., at most 10 UIDs per request), on the session's background connection, or on its own connection whenbackground_connectionis off. - Each message's preview is asked for once per session.
- Maildir accounts have no previews.
On sora, PREVIEW is off unless the IMAP server's [[server]] block in sora's configuration sets enable_preview = true. That block's limits.preview_s3_* keys bound how many messages sora fetches from storage to answer the previews it does not have at hand.
To check, sign in to the IMAP server and look for PREVIEW in its capability list, or see whether the list shows text under the subjects.
The search box searches the folder on screen. On an IMAP account, ALPS turns the query into IMAP search criteria and the mail server does the matching. ALPS does no matching of its own there.
| Query | What it searches |
|---|---|
invoice |
The header and the body (IMAP TEXT) |
from:alice, to:bob, cc:carol, subject:report
|
That header field |
body:"hello world" |
The body only (IMAP BODY) |
is:unread, is:read (or is:seen), is:starred (or is:flagged) |
Unread, read or starred messages |
header:In-Reply-To:"<id@example.com>" |
Any named header field (IMAP HEADER) |
- Write the value straight after the colon:
from:alice, notfrom: alice. Every prefix needs a value, sofrom: alice(an emptyfrom:term followed by the plain wordalice),from:,body:""andheader:X-Custom:are refused with HTTP 400Search terms require a value., and the list reads "Could not load this folder." An empty query still means the whole folder. - Quote a value that has spaces:
subject:"urgent meeting notes". - All terms must match (AND). There is no OR or NOT syntax, and a leading
-is sent as part of the text. - A word with a colon that is not one of the prefixes, such as
10:30or a link, is searched for as it is written. So is any otheris:value, such asis:answered, and aheader:term withoutField:value: each adds a condition, and finds little or nothing. - Plain words typed together are sent as one search string. Whether
urgent invoiceneeds the words side by side or only both present depends on the server. To require each word on its own, quote each one:"urgent" "invoice".
The mail server decides how a term matches (as a substring, or as a word in its full-text index) and which header fields header: can search. Some servers index message bodies in the background after delivery, sora among them, so a word in the body of a message that just arrived may not be found for a short while.
What the mail server's capabilities change:
-
Search All needs
MULTISEARCH(RFC 7377). The search-results banner then offers it, and it searches every subscribed folder (unsubscribed folders are not searched). Its results come as one flat list, by date, with each row naming its folder. On a server without MULTISEARCH, the all-folders view opened from its URL is refused with HTTP 400. - Conversations in the results need a THREAD capability and the user's Settings > Reading & Composing > Use threading. A folder of more than 10,000 messages is listed flat.
-
Date order in a flat list needs
SORT. Without it, results come in the order the messages were filed in the folder (UID order). -
A server that limits searches may refuse one with
NO [LIMIT](RFC 5530). ALPS then answers the listing with HTTP 429{"error":"search_rate_limited"}, plusRetry-Afterwhen the server names a wait, and the list reads "The mail server is limiting searches right now. Try again in a moment."
On a Maildir account, the same syntax applies, prefixes and quotes included, but ALPS does the matching itself, ignoring case. is: looks at the flags in the file names, so the Starred and Unread filters work. The header prefixes look at the decoded header fields, body: at the decoded body of every part, and plain text at both. Search All is not offered. A prefix with no value, such as from:, is refused with HTTP 400, as on an IMAP account. Full syntax: SEARCH.md.
The composer uploads each file to ALPS (POST /attachments), and ALPS holds it in the user's session until a draft save or a send writes it into the message. The composer saves the draft as soon as a batch of uploads finishes, so an upload is normally pending only until that save. Pending uploads live only in the memory and the temp directory of the node that received them. They do not survive a restart and are not shared between cluster nodes. When an upload is gone (deleted to make room, or lost in a restart), the next save or send is refused with 409 missing_attachments, nothing is sent or stored, and the composer lists the files to add again.
Three budgets in [server] bound the pending uploads:
| Key | Default | What it bounds | When an upload does not fit |
|---|---|---|---|
max_attachment_mib |
32 | The pending uploads of one composer. It is an upper bound: the account's limit is lower when the mail servers advertise a message size limit (see below). The web composer checks the whole message against the account's limit before uploading, counting new files and the parts carried from a draft or a forwarded message. Each upload request is capped at that limit + 1 MiB. | The composer says so and uploads nothing. The server refuses the upload with 400, and refuses a request body over the cap with 413. |
max_session_attachment_mib |
128 | All pending uploads of one session, across its composers | The session's oldest pending uploads are deleted to make room |
max_global_attachment_mib |
1024 | All pending uploads on one node | The node's oldest pending uploads are deleted, whichever session holds them |
A value of 0 or below means the default, so none of the budgets can be turned off. In the test lab, with max_attachment_mib = 1, a 500 KB upload was accepted (200), a 1.5 MiB upload was refused with 400, and a 3 MiB upload with 413.
Plan the rest of an upload's path too:
-
The temp directory.
[server] temp_dir, or the system default ($TMPDIR, else/tmp). An upload request keeps its files in memory while they fit in a 32 KiB allowance, and writes the rest to amultipart-*file there, with mode 0600. ALPS does not create the directory at startup, so create it beforehand, owned by the user ALPS runs as and closed to other users: until it exists, every upload over 32 KiB is refused with 500upload_storage_failed, and ALPS logsupload multipart: ...with the cause. Leave room abovemax_global_attachment_mib. Files left behind by a crash or a kill, or by a shutdown whose session cleanup took longer than its 5 seconds, are removed by an hourly job once they are more than 7 days old. The job keeps the files of uploads ALPS still holds, however old, but it cannot tell ALPS's files from another program's files of the same name, so give ALPS its owntemp_dir. While ALPS is stopped, everymultipart-*file in its own temp directory is a leftover and can be deleted. See OPERATIONS.md, The Temp Directory, which also covers systemd'sPrivateTmp. -
HTTP timeouts.
[server] read_timeout_sec(default 10) bounds the whole request, the upload's body included: an upload whose body takes longer is refused with 408upload_timed_out.write_timeout_sec(default 60) runs from the end of the request's headers until the response has been written. For large files over slow links, raise both, or put a proxy in front that buffers request bodies. -
The reverse proxy's body limit. Set it to at least
max_attachment_mib+ 1 MiB, and no less than 32 MiB, the largest message body ALPS accepts for a send or a draft save. nginx's defaultclient_max_body_size 1mrefuses every upload over 1 MiB before ALPS sees it. See Setup Behind a Reverse Proxy. -
The mail servers' message size limits. ALPS writes attachments into the message in base64, which makes them about a third larger, so 32 MiB of files makes a message of more than 42 MiB. The SMTP server must accept a message that size, and the IMAP server an APPEND of it, because ALPS stores the draft in Drafts and the sent copy in Sent. ALPS reads the limits the servers advertise: the IMAP server's
APPENDLIMIT(as a capability, or for the Drafts and Sent folders) and the submission server'sSIZE, which it reads over a short connection without signing in, at most every ten minutes per session. The account's attachment limit, theMaxAttachmentMiBthatGET /sessionandGET /settingsreport, is then the smaller ofmax_attachment_miband what fits under those limits after 64 KiB for headers and text and the base64 overhead: a 25 MiB limit leaves about 18 MiB. Before a save or a send, ALPS also checks the whole encoded message, including whether its copy fits in Sent, so a message is not sent when its copy could not be kept. A message the servers refuse as too large (NO [TOOBIG], or SMTP 552) gets 413message_too_large, and the composer keeps its contents and says "This message exceeds the mail server's size limit. Remove attachments or shorten the message and try again." See Mail Server Requirements.
Parts carried from a saved draft, or from a message being replied to or forwarded, are read from the IMAP server when the message is saved or sent. They never pass through /attachments, so they count against none of the three budgets.
Details: ATTACHMENTS.md.
The interface is translated in the browser into 19 languages: Arabic, Chinese (Simplified and Traditional), Danish, English, French, German, Hindi, Indonesian, Italian, Japanese, Korean, Portuguese (Portugal and Brazil), Russian, Serbian (Cyrillic and Latin), Spanish and Turkish.
-
Each user picks a language in Settings > Localization > Language. The default, Auto, takes the first language in the browser's language order that ALPS has, and English when there is none.
-
Arabic is laid out right to left: the layout mirrors, and arrows and other icons that point along the reading direction are flipped. A message's content takes the direction its author set, or the direction of its first strong letter, whatever the interface language. Fields for addresses, passwords, URLs, numbers and dates are written left to right in every language.
-
The server has no language setting. There is no configuration key and no server-side default, and ALPS does not read
Accept-Language. -
The choice follows the account. ALPS saves the language, time format, date format and first day of the week in the account's settings record, on the IMAP server (see Storage in IMAP METADATA) or, for a Maildir account, in its maildir. They apply in every browser the account signs in on. On an IMAP server without METADATA they last only for the connection. The browser also remembers the last language it used, so the sign-in page is shown in that language.
-
Dates and times. Time Format and Date Format default to Auto, which follows the browser's own locale and not the interface language.
-
The first day of the week is Monday for users who have not chosen one. This makes it Sunday:
[plugin.caldav] options = { week_start = 0 }
The key also takes the strings
"0","1","sunday","sun","monday"or"mon", in any case, and any other value means Monday. It applies even when the CalDAV plugin has no server, and even withenabled = falsein the section. Without that, and with no section settingenabled = true, this section also loads the CalDAV plugin, so users get Settings > Calendar to enter a server of their own (see Plugins). A user who picks a day in Settings keeps their own choice. -
Calendar emails (invitations, replies and cancellations) are written by the server in the language of the user who sends them.
To add or change a translation, see LOCALIZATION.md, Adding a Language. The translations are part of the frontend, so a change needs a rebuilt frontend/dist and binary.
For a message that the receiving mail server passed under DMARC or BIMI, ALPS can show the sender's brand logo beside the sender, in the message list and in the reader, when the sender's domain publishes one (see below). The same verdict drives the sender badge, which shows a sender as verified on a DMARC or BIMI pass, or as failed on a failing DMARC, DKIM or SPF result with no pass. It also drives the warning shown above a message that failed.
The verdict comes from the Authentication-Results field that the receiving server writes into each message. Tell ALPS which servers to trust with their authserv-ids, the first word of the fields they write:
[provider.imap]
server = "imaps://imap.example.com:993"
authserv_ids = ["mx.example.com"]- The ids are matched in any case. Without
authserv_ids, ALPS reads the topmostAuthentication-Resultsfield. On a message that your server did not stamp, that field was written by the sender, so set the key on every deployment. - Under
[provider.multi], setauthserv_idson each IMAP backend. Backends reached throughtemplateorautodiscoverhave none of their own. - Maildir accounts get no verdicts, and so no logos and no badges.
In the test lab, with authserv_ids = ["mx.example.test"], a message stamped mx.example.test; dmarc=pass was marked for a logo. A message whose only field was sender.example; dmarc=pass got no verdict.
Beyond a passing verdict, the page asks for a logo only for a message with a single From address, at a domain that is not one of 20 large freemail domains (such as gmail.com and outlook.com), and only while the user's Settings > Appearance > Show sender avatars is on (the default). ALPS then needs all of the following:
- A DMARC record at
_dmarc.<domain>withp=quarantineorp=reject. ALPS reads that exact name, with no fallback to the organisational domain. - A
v=BIMI1TXT record atdefault._bimi.<domain>whosel=URL starts withhttps://. - A logo that can be fetched within 5 seconds. ALPS reads at most 1 MiB, removes script from the SVG, and serves it as
image/svg+xmlwith a sandboxingContent-Security-PolicyandX-Content-Type-Options: nosniff.
What this asks of the ALPS host:
-
Direct outbound HTTP, HTTPS and DNS. The logo fetch ignores
HTTP_PROXY,HTTPS_PROXYandNO_PROXY, and connects only to public addresses. A host whose web traffic must go through an HTTP proxy shows no logos. See OPERATIONS.md, Outbound Connections. -
A cache in
<temp directory>/alps-avatars. It holds logos, "no logo" answers, and failures to retry after 5 minutes. An hourly job deletes entries older than 7 days, so each domain is looked up again about once a week. The directory can be deleted at any time. The first lookup creates it, and the temp directory too, with mode 0755, so create the temp directory yourself as described under Attachments.
Without a [branding] section, ALPS shows the Alps colour theme and the edelweiss logo. Two keys change that:
[branding]
default_theme = "nord" # "alps", "migadu", "nord" or "ocean"
logo = "/etc/alps/logo" # "alps", "migadu", or a folder holding a favicon package-
default_themeis the colour theme for users who have not picked one in Settings > Appearance > Color Theme. A user's own pick stays theirs, and light or dark still follows each user's Theme Mode. -
logoreplaces the edelweiss on the sign-in page and in the header, and provides the browser tab and home screen icons. It takes a built-in logo by name or a folder holding a favicon package of six files. - A wrong value stops startup with exit status 1 and a line that names the key.
-
The browser tab's title stays
Webmail.[branding]does not change it, and configured branding does not show through the Vite dev server.
To check, curl -s https://webmail.example.com/ | grep alps- shows the branding tags in the page. In the test lab, default_theme = "nord" showed <meta name="alps-default-theme" content="nord" />.
The favicon package's files and limits, the startup errors, how open tabs learn of a change, and examples are on Config-Branding.
The examples on this page come from a test lab: ALPS built from main, in front of a sora IMAP server with IDLE, NOTIFY, CONDSTORE, MULTISEARCH and PREVIEW turned on. Its outputs, such as the event stream, the status answers, the upload status codes, the authentication verdicts and the startup errors, are the ones quoted above. The language and right-to-left behaviour, and the BIMI DNS and logo checks for a domain that has a logo, were checked against the code and LOCALIZATION.md and OPERATIONS.md, not run. The lab ran at commit a5443047. Push through the Vite dev server, the refused searches, the sweep of leftover upload files and the answer to a send whose Sent copy fails were updated from the code at main d1cc02cc, without a rerun. The search terms is:read and the ones searched as text, Maildir search, the attachment limit that follows the servers' advertised limits, the answers for a refused upload (500 and 408) and for a message too large, and the [plugin.caldav] section that leaves enabled out were updated from the code at main 92ec0e11, without a rerun.
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