Repository navigation
Plugins
This page covers the six plugins built into ALPS: what each one adds, what it needs from your servers and configuration, and how the configuration decides which of them load. It ends with a short guide to writing a plugin of your own. The full developer reference is docs/PLUGINS.md.
Almost everything ALPS does comes from a plugin, sign-in and mail included. A plugin is a Go package compiled into the alps binary, and it can have a frontend directory that is bundled into the web app at build time. A built binary cannot take a new plugin: to add one you rebuild ALPS, and to switch one off you change the configuration.
Plugins load once, when ALPS starts. ALPS has no reload, so restart it after you change a [plugin.<name>] section.
The standard build compiles in all six plugins through the blank imports in cmd/alps/plugins.go:
| Plugin | What it adds | What it needs | Reference |
|---|---|---|---|
base |
Sign-in and sign-out, the mail API, Settings, linked accounts, security keys, the new-mail event stream | A mail provider and an SMTP submission server | ARCHITECTURE.md |
caldav |
The Calendar and Tasks tabs, Settings > Calendar, and an invitation banner above messages that carry a calendar part | A CalDAV server: yours, which accepts the user's ALPS sign-in, or one each user enters in Settings | CDAV.md |
carddav |
The Contacts tab, Settings > Contacts, address suggestions in the composer, and saving recipients to the address book | A CardDAV server with an address book for each account: yours, or one each user enters in Settings | CDAV.md |
managesieve |
Settings > Filters and Settings > Auto-reply | A ManageSieve server that accepts the user's ALPS sign-in | FILTERS.md |
gpg |
Settings > GPG Keys, an encryption button in the composer, and decryption in the reader | Account storage that persists, and carddav for recipients' keys |
GPG.md |
password |
Settings > Password | An HTTP endpoint, or a program on the ALPS host, that changes the password | PASSWORD_CHANGE.md |
base serves sign-in and sign-out (POST and DELETE /session) and GET /session, which tells the web app who is signed in and which plugins are loaded. It also serves everything to do with mail: folders, messages, sending, attachments, search, quota and shared folders. On top of that it serves the built-in Settings sections, the account's own CalDAV and CardDAV connections (/settings/dav/caldav and /settings/dav/carddav, for a loaded plugin only), linked accounts, security keys and recovery codes, and the new-mail event stream. The route groups are listed in ARCHITECTURE.md.
-
Needs: a mail provider (
[provider]) and an SMTP submission server ([smtp] server). See Mail-Providers, Config-Providers-and-SMTP and Mail-Server-Requirements. -
Configuration: none. It has no
serverand no options, and it always loads, whatever its section says. -
Frontend:
basehas no plugin frontend directory. Its UI is the core app infrontend/src. -
Background job:
base:cleanup_bimi_avatarsdeletes cached BIMI logos older than 7 days, once an hour.
caldav adds the Calendar and Tasks tabs. Tasks are the to-dos (VTODOs) kept in the same calendars. It also shows a banner above a message that carries an invitation, so the user can accept, mark it tentative, decline, or apply an update. If the CalDAV server does not deliver invitations itself, ALPS sends them as email through the user's SMTP submission server.
- Needs: a CalDAV server that accepts HTTP Basic authentication. The server you configure is the default: ALPS signs in to it with the login and password the user signed in to ALPS with, unless the user saves a separate username and password for it.
- Settings > Calendar: each user can save a connection of their own there: a separate username and password for the default server, or a CalDAV server of their own (see CalDAV and CardDAV connections in Settings).
-
Configuration:
servertakes a URL, and is optional. Usehttps://orcaldavs://for TLS. The plain-HTTP forms (http://,http+insecure://,caldav+insecure://) send the password unencrypted, so keep them for a server on the same host. A bare domain with no scheme, port or path (example.com) makes ALPS look up the server through DNS SRV (_caldavs._tcp) once, at startup. If that lookup fails, ALPS logscaldav: failed to discover CalDAV server: ...and loads the plugin without a default server. Any other value without a scheme, such asdav.example.com:8443ordav.example.com/caldav, stops startup. The one option,week_start(aliasfirst_day_of_week), sets the first day of the week for users who have not chosen their own:0or"sunday"for Sunday,1or"monday"for Monday, which is the default. -
Per domain: under the
multiprovider, each backend can set its owncaldav =endpoint (see Setup-Many-Domains).
[plugin.caldav]
enabled = true
server = "https://dav.example.com/"
options = { week_start = 0 } # weeks start on Sundaycarddav adds the Contacts tab. It also suggests addresses from the contacts as the user types a recipient in the composer, and it saves the recipients of sent mail to the address book. The gpg plugin looks up recipients' public keys through it.
-
Needs: a CardDAV server that accepts the same HTTP Basic sign-in, the default one or one the user enters in Settings > Contacts, as for
caldav. ALPS uses only the first address book the server lists for the account, and never creates one, so the server must provide one for each account. Without one, Contacts lists nothing and a contact cannot be saved. -
Configuration:
serveris optional, and must be a full URL with a scheme:https://orcarddavs://for TLS, or one of the plain-HTTP formshttp://,http+insecure://andcarddav+insecure://, which send the password unencrypted. Thecaldavschemes stop startup here (unknown scheme for CardDAV server). There is no discovery, and a bare host name stops ALPS at startup withCardDAV server requires a scheme (https://, http+insecure://), got: //carddav.example.com.carddavreads no options. -
Per domain:
carddav =in amultibackend.
[plugin.carddav]
enabled = true
server = "https://dav.example.com/"With caldav or carddav loaded, Settings > Calendar and Settings > Contacts let each user set up the account's own connection, for each of the two separately:
-
Server URL. Left blank, the account uses the default server: the plugin's
server, or the endpoint the user'smultibackend names. Otherwise it is a server of the user's own, given withhttps://, or withcaldavs://under Calendar andcarddavs://under Contacts. Without a default server for the account, a URL is required. - Authentication. The mail username and password, or a separate username and password. Before the mail password goes to a server the user entered, Settings asks for it again, and ALPS checks it. That check counts against the login rate limits.
- Save connection checks the connection first: ALPS looks up the user's calendar or address-book home on the server, within 10 seconds, and saves nothing when that fails. Remove connection goes back to the default server with the mail credentials.
What this means for you:
- A default server is optional. A user with none, neither the plugin's
servernor amultiendpoint, sees only the plugin's section in Settings. Calendar and Tasks, or Contacts, appear once the user has a server, and so do the plugin's hooks: the invitation banner, or the address suggestions and saved recipients.GET /sessionlists such a plugin inEnabledPluginsbut not inActivePlugins. - Separate passwords need
[server] login_key: ALPS stores them encrypted with it. Without the key, Settings offers only the mail credentials. After the key changes, a saved separate password can no longer be used, and the user enters it again. - ALPS connects to the servers users enter over HTTPS only, at public addresses only, and without an HTTP proxy.
[server] dav_allow_private_servers = truealso allows the plain-HTTP schemes (http+insecure://,caldav+insecure://,carddav+insecure://) and any address, for users who keep their DAV servers on a trusted internal network. Set it the same on every node. The servers in your configuration are never restricted. - The connection is kept with the account's other records on the mail server (see Storage-in-IMAP-METADATA). A session reads it again at most every 30 seconds.
The full rules are in CDAV.md.
managesieve adds two Settings sections, Filters and Auto-reply. Both edit the account's active Sieve script on the mail server, which the server runs when it delivers mail. Filters has a visual rule editor and a raw Sieve editor. The auto-reply is a vacation rule in the same script.
-
Needs: a ManageSieve server (port 4190 unless the URL names another) that accepts SASL PLAIN with the user's ALPS sign-in. ALPS upgrades the connection with STARTTLS and checks the certificate. If the server does not offer STARTTLS, ALPS does not sign in. The auto-reply needs the server's
vacationextension, and its start and end dates also needdateandrelational. ALPS checks a script before it uploads it, and the server checks it too, so a script that needs an extension the server lists but ALPS does not know, such asinclude, can be saved. -
Loading: the plugin loads only when it has a
server, or when amultibackend names amanagesieveendpoint. A section with neither does not load it. When theenabledrules let the plugin through, startup then logsManageSieve enabled without a server or routed endpoint; plugin disabled. -
Configuration:
servertakesmanagesieve://,managesieves://,sieve://or a bare host name, all of them STARTTLS.managesieve+insecure://andsieve+insecure://allow a connection without TLS and skip the certificate check; keep them for a server on the same host or a trusted local network. Any other scheme stops startup, and so does a host and port with no scheme (mail.example.com:4190), so writemanagesieve://mail.example.com:4190. -
Per domain:
managesieve =in amultibackend. There is no DNS discovery.
[plugin.managesieve]
enabled = true
server = "managesieve://mail.example.com:4190"gpg adds OpenPGP encryption. In Settings > GPG Keys a user generates or imports a keypair, or purges it. A lock button in the composer turns encryption on for a message, which ALPS then signs and encrypts to every recipient and to the sender. In the reader, an encrypted message is decrypted after the user enters their passphrase.
All of the cryptography runs in the browser. The server stores the user's keyring, with the private key encrypted under the user's passphrase, and never receives the passphrase.
-
Needs:
- Storage that persists. With the IMAP provider the keyring is kept in IMAP METADATA, and with the maildir provider in the user's
alps_store.json. On an IMAP server without METADATA, the keyring lasts only as long as the IMAP connection (see Storage-in-IMAP-METADATA). - The
carddavplugin, with a CardDAV server for the user. Recipients' public keys come from the Public Key field of their contacts. Without one, an encrypted message to anyone other than yourself is not sent.
- Storage that persists. With the IMAP provider the keyring is kept in IMAP METADATA, and with the maildir provider in the user's
-
Configuration: only
enabled. It has noserverand no options. - Limits: only the message body is encrypted, as inline PGP. Encryption does not cover the subject, the addresses, the other headers or the drafts saved while you write. A message with attachments cannot be sent encrypted.
password adds Settings > Password and the route behind it, POST /password/change. ALPS checks the current password, then passes the change to an HTTP endpoint or runs a program on the ALPS host. If the change succeeds, the user stays signed in.
- Needs: something that changes passwords for your mail server: an HTTP API, or a command line tool.
-
Configuration:
[plugin.password] enabled = true, plusendpointorcommandin[plugin.password.options]. Amultibackend can carry its own block. Withenabled = true, ALPS checks the global block'scommandsettings at startup and refuses to start when they fail. For example, setting bothendpointandcommandstops it withplugin.password: set either endpoint or command, not both. The HTTP options are not checked at startup. Commands need a Unix host: on Windows, acommandin the global block stops startup, and one in amultibackend's block fails when it is used. See Setup-Password-Change and PASSWORD_CHANGE.md. -
Loading: the plugin loads only with
enabled = trueand anendpointorcommand, in the global block or in amultibackend's block. Unlike the other plugins, a[plugin.password]section that leavesenabledout keeps it off. Withoutenabled = true, Settings has no Password section and there is no/password/changeroute. Withenabled = truebut nothing to change passwords with, the plugin stays off too, and startup logspassword: enabled without an endpoint, command or routed backend; plugin disabled. Becauseenabled = truehere also turns on the allow-list (below), setenabled = truefor every other plugin you want as well.
Three things decide whether a plugin runs:
- It is compiled in. All six plugins are in the standard build.
-
The
enabledrules let it through.basealways passes. A plugin whose section saysenabled = falsedoes not. If at least one[plugin.<name>]section setsenabled = true, those sections form an allow-list, and onlybaseand the plugins whose own sections setenabled = truepass. If no section setsenabled = true, which includes having no sections at all, every other plugin passes, whether its section leavesenabledout or it has no section. -
It has what it needs.
gpgneeds nothing more.caldavandcarddavneed a section of their own, with or without aserver, or amultibackend that names their endpoint.managesieveneeds aserveror amultibackend that names its endpoint.passwordneedsenabled = truein its own section and anendpointorcommand(see password).
A plugin that step 2 keeps out is not set up at all: ALPS does not read its server or options, so a value there that ALPS would refuse does not stop startup, and its background jobs do not run. Leaving enabled out of a section keeps that plugin's defaults: only enabled = true puts a plugin on the allow-list, and only enabled = false keeps one out.
The safe pattern is to list every plugin you want with enabled = true, as config.example.toml does, and to set enabled = false on the ones you do not use, or leave their sections out. A [plugin.base] section cannot switch base off, but an enabled = true in it turns on the allow-list like any other. Config-Plugins shows that layout.
With the imap or maildir provider, these configurations give these results:
| Configuration | Plugins that load | Result |
|---|---|---|
No [plugin.*] sections |
base, gpg
|
Mail works. GPG keys work, but without carddav encrypted mail to others is not sent. Settings has no Password section. |
Only [plugin.caldav], with a server and no enabled
|
base, caldav, gpg
|
No section sets enabled = true, so there is no allow-list, and gpg loads beside caldav. |
gpg with enabled = true, and [plugin.caldav] with a server and no enabled
|
base, gpg
|
caldav is dropped: once a section sets enabled = true, a section that leaves enabled out does not load its plugin. |
base and gpg with enabled = true, password with enabled = false
|
base, gpg
|
As intended. |
Only gpg with enabled = true
|
base, gpg
|
As intended: base loads without being listed. |
base, caldav, carddav, managesieve and gpg with enabled = true, no server lines |
base, caldav, carddav, gpg
|
ManageSieve stays off, and Settings has no Filters or Auto-reply. Settings has Calendar and Contacts sections, and a user gets the Calendar, Tasks and Contacts tabs once they enter servers of their own there. |
A caldav or carddav plugin that is loaded but has no server for a user, neither a global server nor a per-domain endpoint, shows that user only its section in Settings (see above). Under multi, a managesieve plugin loaded for the backends that name an endpoint shows Filters and Auto-reply to the other users too, and their requests fail, logged as ManageSieve server is not configured. Give the plugin a server, or switch it off.
Every plugin's frontend is in the bundle that ALPS serves, whether or not the plugin loads. After sign-in, the app reads two lists from GET /session: EnabledPlugins, the plugins that loaded, and ActivePlugins, those of them the account can use, which leaves out caldav and carddav for an account without a server for them. Settings shows the sections of the plugins in EnabledPlugins, and the app shows the tabs, pages and hooks only of the plugins in ActivePlugins. Switching a plugin off therefore needs only a configuration change and a restart, never a frontend rebuild.
At startup ALPS logs Loaded plugin "<name>" for each plugin that loads. A plugin that is not set up, because the enabled rules keep it out or it lacks what it needs, gets no line of its own, except managesieve and password with nothing to connect to, which log the lines quoted above. For each section that sets enabled = true but whose plugin did not load, ALPS warns Enabled plugin "<name>" did not load; check its name and backend configuration. That warning also catches a misspelled section name, such as [plugin.calddav] with enabled = true. A signed-in browser sees the same list in the EnabledPlugins field of GET /session. With only base and gpg enabled, on mail servers that advertise no message size limit, the answer is:
{"ActivePlugins":["gpg","base"],"Address":"ada@example.test","EnabledPlugins":["gpg","base"],"MaxAttachmentMiB":32,"PasswordChangeEnabled":false,"Username":"ada@example.test"}MaxAttachmentMiB is the account's attachment limit: [server] max_attachment_mib, or less when the mail server advertises a message size limit (see Features).
A plugin configuration error stops startup with an error message that starts failed to load plugins:, but only for a plugin the enabled rules let through. Errors of this kind include a CalDAV or CardDAV server that is not a URL ALPS takes, such as a carddav server without a scheme, a ManageSieve scheme ALPS does not know, and an enabled password block whose command settings fail the startup checks. The server lines the DAV and ManageSieve plugins log, and the reachability check each DAV plugin makes at startup, are on Config-Plugins.
A new plugin is a directory under plugins/, compiled into your own build of ALPS. You need the Go toolchain and Node.js. The password plugin is the smallest complete example: one route, one Settings section and its translations.
plugins/myplugin/
├── plugin.go # the Go side: routes, loader, jobs
├── plugin_test.go # Go tests sit beside the sources
├── frontend/
│ ├── index.ts # registers tabs, routes and hooks
│ ├── myplugin-settings.ts
│ └── i18n/ # en.ts and one file for each language
└── public/assets/ # optional static files, served from disk
Define the plugin with alps.GoPlugin, register its routes, and register a loader from init():
package myplugin
import (
"net/http"
"github.com/migadu/alps"
)
var plugin = alps.GoPlugin{Name: "myplugin"}
func init() {
plugin.GET("/myplugin/status", handleStatus)
plugin.POST("/myplugin/note", handleNote)
alps.RegisterPluginLoader(func(s *alps.Server) ([]alps.Plugin, error) {
cfg := s.Options.Plugins["myplugin"]
if !cfg.Enabled {
return nil, nil // stay off without a [plugin.myplugin] section, or with enabled = false
}
return plugin.Loader()(s) // applies the enabled rules, the allow-list included
})
}
func handleStatus(ctx *alps.Context) error {
return ctx.JSON(http.StatusOK, map[string]string{"user": ctx.Session.Username()})
}
func handleNote(ctx *alps.Context) error {
var req struct {
Text string `json:"text"`
}
if err := ctx.BindJSON(&req); err != nil {
return ctx.RespondBindError(err)
}
if req.Text == "" {
return alps.NewHTTPError(http.StatusBadRequest, "text is required")
}
if err := ctx.Session.Store().Put("myplugin_note", req.Text); err != nil {
return err
}
return ctx.NoContent(http.StatusNoContent)
}Then add a blank import to cmd/alps/plugins.go:
_ "github.com/migadu/alps/plugins/myplugin"How the Go side behaves:
-
The name is the key everywhere.
Nameis the plugin's[plugin.<name>]section, its entry inEnabledPluginsandActivePlugins, and thepluginIdits frontend uses. -
Routes are
net/httppatterns such as/myplugin/items/{id}, read withctx.Param("id"). Give them a prefix named after the plugin. A pattern that conflicts with an existing one panics at startup. -
Every route runs behind the router's middleware.
POST,PUT,PATCHandDELETErequests must pass theOrigin/Referercheck, so make any route that changes something use one of those methods, notGET. Requests without a signed-in session, or whose session still needs its second factor, are answered 401 before your handler runs, soctx.Sessionis always set inside it. -
Errors: return an
alps.NewHTTPError(status, "message")to answer with that status and message. ALPS also recognises a refused mail password (401), an unreachable mail server (503) and a message that is gone (404), also inside an error that wraps them, so wrap with%w, not%v. Other errors are answered with a generic 500, and the detail goes only to the log.ctx.BindJSONreads at most 1 MiB. -
Configuration:
s.Options.Plugins["myplugin"](orctx.Server.Options.Pluginsin a handler) holdsEnabled,ServerandOptions, the free-form[plugin.myplugin.options]table.Enabledis true for a section that setsenabled = trueor leaves it out, and false for one that saysenabled = falseand when there is no section. A loader stays off by returning no plugins, and stops startup by returning an error. Before it reads its options, a loader should asks.PluginEnabled("myplugin"), as the shipped loaders do, so that a plugin theenabledrules keep out is not set up and cannot stop startup.plugin.Loader()asks it too. ALPS warns at startup about any key in the section other thanenabled,serverandoptions, but never about the keys insideoptions. -
Per-user data:
ctx.Session.Store()keeps JSON values under string keys for the account. With the IMAP provider it is kept in IMAP METADATA, and with the maildir provider inalps_store.json, so it follows the user to every browser and every node. On an IMAP server without METADATA it does not persist. Prefix your keys with the plugin's name so they stay apart from the ones ALPS keeps. See Storage-in-IMAP-METADATA. -
Static files in
plugins/<name>/public/assetsare served at/plugins/<name>/assets/.... ALPS reads them from disk relative to its working directory and serves them without a session, so put nothing there that needs a sign-in. The shipped service files run ALPS in/(the systemd unit sets noWorkingDirectory, and the FreeBSD script starts ALPS withdaemon -c), so a plugin with static files needsWorkingDirectoryset in a systemd drop-in, or its files placed under/plugins/<name>/public/assets. -
Lifetime: ALPS loads plugins once and never reloads them. At shutdown it calls the
Closemethod of each plugin that loaded, once. A plugin that a loader returned but theenabledrules drop is closed at once, at startup. AGoPlugin'sClosedoes nothing.
Routing per domain under multi, background mail work and the full error mapping are in docs/PLUGINS.md.
At build time, Vite imports every plugins/*/frontend/index.ts into the bundle. Each one runs when the app starts, even when its Go plugin is switched off; only what it registers is filtered. An index.ts registers what the plugin adds with the plugin registry:
import { registry } from '../../../frontend/src/plugin-registry';
registry.registerSettingsTab({
id: 'myplugin', // equals the Go plugin's Name
labelKey: 'settings.categories.myplugin',
icon: 'star', // a <symbol> id in the icon sprite
component: 'alps-myplugin-settings',
load: () => import('./myplugin-settings'), // fetched when the section first opens
});
registry.registerHook('composer:send', async ({ recipients }: { recipients: string[] }) => {
try {
console.debug('myplugin: sent to', recipients.length, 'recipients');
} catch (e) {
console.error('myplugin: composer:send failed', e);
}
}, 'myplugin');The registry has registerSettingsTab (a section of the Settings window), registerRoute and registerNavTab (a full page, such as Calendar), and registerHook. How the registry treats them:
-
Matching: settings tabs are matched to the server's
EnabledPlugins, and navigation tabs, routes and hooks to itsActivePlugins. Tabs are matched bypluginId, or byidwhen there is nopluginId; routes and hooks bypluginIdalone. SetpluginIdto the Go plugin'sNamewhenever an id differs from it. A route or hook without apluginIdcounts as part of the core app and always runs. - Duplicates: a second tab with an id already registered, or a second route with a path already registered, is ignored.
-
Icons: an
iconmust be the id of a<symbol>infrontend/public/assets/icons/sprite.svg. An unknown id draws nothing. -
Loading: register components with
load: () => import(...), so that they are fetched when first shown rather than with every user's first download. -
Translations: these go in
plugins/myplugin/frontend/i18n/<code>.ts, one file for each of the 19 languages ALPS ships, each withexport default { ... }. The keys are merged into the app's dictionary. The frontend tests require each file to have exactly the keys ofen.ts, with its language's plural forms anden.ts's placeholders. See LOCALIZATION.md. -
API calls: call your Go routes with
fetchWithTimeoutfromfrontend/src/utils/fetch-utils. It handles an expired session, timeouts, retries of aGETand the offline banner. It resolves with a non-2xx answer rather than throwing, so checkresponse.ok.
Settings sections with their own Save button, page routes and nav tabs, and Web Components are covered in docs/PLUGINS.md.
The app invokes five hooks. Pass the plugin's name as the third argument to registerHook, so that the handler runs only while the plugin is enabled.
| Hook | When it runs | What the handler returns |
|---|---|---|
composer:toolbar |
Each time the composer toolbar is drawn | A Lit template for the toolbar (synchronously) |
composer:presend |
Before the send request | A FormData to replace the request body, or false to cancel. A handler that fails also cancels the send. |
composer:send |
After the server accepted the message | Nothing; the result is ignored |
composer:suggest |
While the user types an address | An array of { name, address } suggestions |
reader:content |
Before a message body is shown | A replacement body, or nothing. Banners go onto payload.banners. |
The built-in plugins use them this way: carddav uses composer:suggest and composer:send, gpg uses composer:toolbar, composer:presend and reader:content, and caldav uses reader:content for the invitation banner. Payloads and failure handling are in docs/PLUGINS.md.
plugin.AddJob("name", func() error { ... }) registers a job that runs once an hour, under the name <plugin>:<name>. The first run comes one hour after ALPS starts. The interval cannot be configured. ALPS logs a job that returns an error or panics, and runs it again on the next tick. ALPS schedules a plugin's jobs only once the plugin has loaded, so a plugin that the enabled rules keep out runs none.
- Run
make build-frontend(npm install && npm run buildinfrontend/). The build type-checks everyplugins/*/frontendfile. - Run
make build, because the built frontend is embedded in the binary. - Add a
[plugin.myplugin]section to your configuration. If any other section setsenabled = true, asconfig.example.tomldoes, setenabled = truein yours as well, or the allow-list keeps your plugin out.
For development, npm run dev in frontend/ proxies the API to ALPS at http://localhost:1323, but only for the path prefixes listed in frontend/vite.config.ts. Add your plugin's prefix there.
Tests:
-
Go tests go beside the plugin (
plugins/myplugin/*_test.go) and run withmake test. See Go-Tests. -
Frontend unit tests go in
frontend/test/, not in the plugin directory, because Vitest only collectsfrontend/test/**/*.test.ts. Import the plugin from../../plugins/myplugin/frontend/..., and run them withnpm testinfrontend/, which typechecks the tests before it runs them. See Frontend-Unit-Tests. -
Browser tests go in
frontend/e2e/. The test configuration enables plugins by name withenabled = true, which is an allow-list, so a new plugin needs its own[plugin.myplugin]block withenabled = trueinscripts/e2e.shbefore those tests can load it. See Browser-Tests.
-
Config-Plugins: every
[plugin.<name>]key. - Architecture-Overview: where plugins sit in ALPS.
- Features: what users see.
- docs/PLUGINS.md, docs/CDAV.md, docs/FILTERS.md, docs/GPG.md and docs/PASSWORD_CHANGE.md.
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