evopixel / stalwart
Stalwart account management for Roundcube
Package info
github.com/evopixelro/roundcube-stalwart
Type:roundcube-plugin
pkg:composer/evopixel/stalwart
Requires
- php: >=7.4.0
- ext-json: *
- ext-openssl: *
- ext-xmlwriter: *
- bacon/bacon-qr-code: ^2.0.0 || ^3.0.0
- guzzlehttp/guzzle: ^7.3.0
- roundcube/plugin-installer: ~0.3.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Manage Stalwart account security and email addresses from Roundcube Settings. An independent community plugin by EvoPixel, licensed under GPL-3.0-or-later.
Version 1.1.0 was integration-tested against Stalwart 0.16.26 with isolated accounts and Roundcube 1.6.19.
Features
- Account-password changes using the existing Password plugin's policies.
- TOTP two-factor setup and removal, with locally generated QR codes.
- Application passwords: create, display once and revoke.
- Email aliases for accounts with existing administrative permissions.
- Public OpenPGP/S/MIME keys: import, rename and delete.
- Masked email: create, enable/disable and delete on Stalwart Enterprise.
- Optional native login with a second Stalwart TOTP step before mailbox access.
- Remember me for native Stalwart login with the Persistent Login plugin.
Features are independently configurable and disabled by default. The plugin uses native settings pages and multilingual catalogs with native English fallback. No core, skin, built-in Password plugin or database patches are needed.
The 80 language catalogs cover Roundcube 1.6/1.7 through native fallbacks. Login and two-factor messages are included in all 80 language catalogs. New translations are machine-generated; native-speaker corrections are welcome. Dari uses the Persian fallback. Asturian, Interlingua, Kabyle, Norwegian Nynorsk and Talossan currently use English.
Compatibility
Supported Roundcube series: 1.6.x, starting at 1.6.0, and 1.7.x. Support targets both series, not just the individual releases listed below. The plugin uses Roundcube's public plugin API, with no checks that restrict it to specific patch releases. New 1.6.x and 1.7.x patches are expected to work while those APIs and the declared dependencies remain compatible; each future release still needs regression testing.
Roundcube 1.8 and later: the plugin may work if the same APIs remain available, but these versions are not yet tested or included in the supported range. Support will be reassessed when they are available. Versions before 1.6.0 are unsupported.
Tested releases
The following are test coverage examples, not an exhaustive compatibility list.
| Roundcube | PHP used | Checks |
|---|---|---|
| 1.6.0 | 7.4 | Offline API/policy suite and native plugin lifecycle |
| 1.6.19 | 8.2 | Offline suites, Composer installation, browser fixtures |
| 1.7.0 | 8.2 | Offline API/policy suite and native plugin lifecycle |
| 1.7.4 | 8.2 | Offline suites, Composer installation, browser fixtures |
Older patches are compatibility fixtures, not deployment recommendations. Use a current security release from Roundcube downloads.
Runtime and integration requirements
The plugin requires PHP 7.4+, JSON, OpenSSL and XMLWriter. Follow any stricter requirements of Roundcube itself: 1.7 requires PHP 8.1+. Composer resolves Guzzle and BaconQrCode (versions 2 or 3) in Roundcube's dependency environment.
Browser checks cover stock Elastic and compatible custom skins on desktop/mobile.
Other skins must implement the native plugin/settings UI and have not all been
tested. Roundcube 1.7 requires its normal public_html document root and asset
router. This plugin adds no public PHP endpoint or extra rewrite requirement.
The Stalwart contract was checked against 0.16.25 and integration-tested on
0.16.26, using the current x:*
JMAP management API, not legacy REST endpoints. Password/TOTP management targets
Stalwart's internal directory, not credentials managed by external directories.
The optional embedded login targets 0.16.26 and its /api/auth OAuth flow.
Install
From the Roundcube root, where its composer.json is located:
composer require "evopixel/stalwart:1.1.2"
The package and its dependencies are available on Packagist. No extra
repositories entry is required. Composer ignores repository declarations inside
dependencies; custom repositories belong to the application root.
To follow development instead, use evopixel/stalwart:dev-main.
Allow the trusted roundcube/plugin-installer Composer plugin when prompted.
It converts the package name to plugins/stalwart and normally
creates config.inc.php from the distributed example. Check Roundcube's plugin
list and append the following once if the installer has not already enabled it:
// Keep the other entries in your existing plugins array. $config['plugins'][] = 'stalwart';
Configure plugins/stalwart/config.inc.php. Copy config.inc.php.dist
to that path first if the installer did not create it. Roundcube also supports
stalwart.inc.php in its configuration directory when no local plugin
file exists.
Local plugin configuration overrides the main Roundcube configuration. A
generated local file with false values overrides the same options set to true
in the main configuration. Avoid defining an option in multiple locations.
A manual installation must use the same directory name and install the declared
dependencies through Roundcube's root Composer environment. Do not bundle a second
Roundcube installation or a separate plugin vendor directory.
Configuration
| Option | Default | Purpose |
|---|---|---|
stalwart_enabled |
false |
Master switch |
stalwart_hosts |
null |
Per-server settings keyed by exact IMAP address; required for multiple servers |
stalwart_api_url |
'' |
Trusted HTTPS JMAP URL for the mailbox's server |
stalwart_twofactor_enabled |
false |
TOTP setup/removal |
stalwart_app_passwords_enabled |
false |
Application passwords |
stalwart_aliases_enabled |
false |
Alias administration |
stalwart_public_keys_enabled |
false |
Public-key management |
stalwart_masked_email_enabled |
false |
Enterprise masked email |
stalwart_login_enabled |
false |
Native username/password and optional TOTP login |
stalwart_login_client_id |
'' |
Dedicated registered Stalwart OAuth client |
stalwart_login_client_secret |
'' |
Client secret, if required by its registration |
stalwart_login_redirect_uri |
'' |
Exact registered HTTPS callback URI |
For example, in the local plugin configuration:
$config['stalwart_enabled'] = true; $config['stalwart_api_url'] = 'https://mail-server.example.com/jmap'; $config['stalwart_twofactor_enabled'] = true; $config['stalwart_app_passwords_enabled'] = true;
Enable other features only after reviewing the permission requirements below. Disabled features have no settings entry or registered feature actions. The master switch and URL are required for all features. Disabling the TOTP management page does not bypass OTP verification on password changes.
Multiple mail servers
Keep IMAP and SMTP servers configured in Roundcube. Use the exact imap_host
addresses as keys in stalwart_hosts, in the local Stalwart configuration:
$config['stalwart_enabled'] = true; $config['stalwart_hosts'] = [ 'ssl://mail.example.org:993' => [ 'api_url' => 'https://mail.example.org/jmap', 'twofactor_enabled' => true, 'app_passwords_enabled' => true, 'password_driver' => 'stalwart', ], 'ssl://mail.example.net:993' => [ 'api_url' => 'https://mail.example.net/jmap', 'twofactor_enabled' => true, 'login_enabled' => true, 'login_client_id' => 'YOUR_REGISTERED_CLIENT_ID', 'login_redirect_uri' => 'https://webmail.example.org/index.php/login/oauth', ], ];
Unlisted servers do not use the plugin. API endpoints and login client credentials
must be supplied in each entry; they never fall back to another server's values.
Feature flags inherit the global defaults unless overridden. A per-server
password_driver takes priority over Password's shared configuration.
With stalwart_hosts = null, the existing global settings work for one IMAP server;
multiple servers require an explicit map.
Login uses the selected configured server. Account settings use the authenticated IMAP session, including its port and TLS mode, regardless of URL parameters. Settings forms and pending two-factor challenges are bound to their server and account. Configuration changes invalidate the old binding; sign out and back in after changing an endpoint or client registration.
Roundcube controls redirect OAuth and the server selector. This map does not add
OAuth providers to Roundcube or change its login policy. Leave login_enabled
disabled on servers using redirect OAuth; their two-factor prompt belongs to the
provider. The plugin's settings remain available after either login method.
Embedded login
This optional mode keeps sign-in inside the active Roundcube skin. Accounts without TOTP proceed after username/password verification. When Stalwart requests MFA, Roundcube asks for the six-digit code before opening an IMAP connection. Stalwart verifies the code; the plugin never reads the account's TOTP seed.
Register a dedicated OAuth client on Stalwart with the exact HTTPS callback URI
configured below. Allow the openid, offline_access and
urn:ietf:params:oauth:scope:mail scopes. Configure each IMAP server and its SMTP transport
on the same Stalwart deployment, with TLS for remote connections and XOAUTH2
support. The JMAP URL must end in /jmap; its HTTPS origin must match Stalwart's
configured issuer. Embedded sign-in does not redirect the browser to the callback,
but Stalwart still validates it when issuing the authorization code and token.
$config['stalwart_enabled'] = true; $config['stalwart_api_url'] = 'https://mail-server.example.com/jmap'; $config['stalwart_login_enabled'] = true; $config['stalwart_login_client_id'] = 'YOUR_REGISTERED_CLIENT_ID'; $config['stalwart_login_redirect_uri'] = 'https://webmail.example.com/index.php/login/oauth'; // Set stalwart_login_client_secret if the registered client requires a secret.
Use this mode only on servers without active Roundcube redirect OAuth. For
external-provider login instead, leave stalwart_login_enabled disabled and use
Roundcube's native OAuth settings. The settings pages work with either mode.
External-provider login forms follow Roundcube's configuration and rendering;
this plugin does not change other providers' login forms.
Embedded sign-in respects Roundcube's username_domain and
username_domain_forced before sending credentials to Stalwart. A domain map is
matched against the selected IMAP hostname, without its scheme or port. Full email
addresses keep their domain unless forcing is enabled. Configure these options in
Roundcube's main configuration; the plugin does not try other domains on failure.
Pending challenges expire after five minutes or five attempts. The password is encrypted in the server-side session while awaiting the code, then discarded on completion, cancellation or failure. Expired pending data is removed on the next request or by normal session garbage collection. PKCE S256 binds the code exchange; access and refresh tokens use Roundcube's session encryption. A rejected token refresh ends the mailbox session. Existing sessions should be signed out when switching login modes; there is no database schema change.
API and lifecycle checks use mocked responses. The browser fixture uses the real Roundcube form and plugin with a simulated OAuth/IMAP server. Verify registration, IMAP, SMTP, token renewal and logout against your Stalwart deployment before enabling this mode in production.
Remember me
Embedded Stalwart login supports Remember me on standard Roundcube. Enable
Persistent Login (persistent_login)
alongside stalwart; either order is supported and no plugin patches are needed.
Its checkbox and translations are reused, and the choice is preserved through TOTP.
A refresh token is required to remember a completed login.
The browser stores a random identifier. Refresh credentials are encrypted in the existing Roundcube database cache and bound to the selected Stalwart server. Restoring a session obtains fresh authorization and rotates the identifier. Logout removes the remembered login; expired or revoked credentials require signing in again.
Set ifpl_login_expire in the Persistent Login configuration (seconds; default three
days, maximum 30 days, zero disables Stalwart persistence). Token renewal does not
extend this expiry. Clearing the database cache removes remembered logins. No extra
table or Roundcube modification is required. Without persistent_login, ordinary
Stalwart login and account settings continue to work.
Password provider
To use this plugin for password changes, set the driver in the existing Password configuration, replacing its previous value:
$config['password_driver'] = 'stalwart';
Keep the bundled password plugin enabled when using its local configuration or
first-login forced-password policy. If the driver is already selected in the
main or Stalwart configuration, this plugin can load Password automatically.
For single-server settings, Password's local configuration has priority and is
checked again after loading. Explicit per-server driver settings take precedence.
Selecting another provider, such as Mailcow, leaves that provider's page unchanged.
The TOTP entry follows Password regardless of the selected provider.
Length, encoding, strength, force-save, host/login restrictions, disabled state,
logging, expiry and password_change hooks retain the built-in Password plugin's
semantics. A missing configured strength driver fails closed. The main password
is required even if password_confirm_current is false, because the API requires it.
Authentication and permissions
Use a trusted HTTPS endpoint for the same server as the signed-in mailbox. Requests use the current user's refreshed OAuth token or session credential. No shared administrator key is needed or accepted. TLS verification is mandatory, redirects are refused, timeouts are five seconds to connect and 15 seconds per request, and responses are limited to 64 KiB.
For OAuth, use Roundcube's standard oauth_* settings and callback routing. No
extra callback is introduced. Password and TOTP changes also require the main
account password and, when 2FA is active, its six-digit code. Do not append the
code to the password.
| Feature | Stalwart permissions |
|---|---|
| Password / TOTP | sysAccountPasswordGet, sysAccountPasswordUpdate |
| Application passwords | sysAppPasswordGet, sysAppPasswordCreate, sysAppPasswordDelete |
| Aliases | sysAccountSettingsGet, sysAccountGet, sysAccountUpdate, sysDomainGet |
| Public keys | sysAccountSettingsGet, sysPublicKeyGet, sysPublicKeyQuery, sysPublicKeyCreate, sysPublicKeyUpdate, sysPublicKeyDestroy |
| Masked email | sysAccountSettingsGet, sysMaskedEmailGet, sysMaskedEmailQuery, sysMaskedEmailCreate, sysMaskedEmailUpdate, sysMaskedEmailDestroy; Enterprise license |
IMAP/SMTP permissions are also needed for Webmail. The plugin never grants permissions and accepts no submitted account ID. Registry queries and mutations verify ownership, including for accounts with impersonation privileges. The server remains the final authorization authority.
Ordinary users normally cannot administer aliases through the current API. Its Account/Domain endpoints require broader administrative permissions. Keep aliases disabled for ordinary users; do not grant broad privileges just for this page. A UI restriction does not restrict what that credential can do elsewhere.
Feature behaviour
TOTP and account passwords
Enabling TOTP from a main-password Webmail session is rejected before any write, because subsequent IMAP authentication would stop working. Sign in with an app password or OAuth first. Primary-password sessions switch to the new password; application-password sessions keep their own credential. Embedded login obtains new OAuth tokens after a password change, using the new password and the submitted TOTP code when required. The mailbox session and Remember me remain active; the new password is not retained in the OAuth session.
If new authorization fails or needs another verification step, the password change remains effective and the user is asked to sign in again, with remembered credentials cleared. Native redirect OAuth must repeat the provider's authorization flow. Tokens revoked by password changes elsewhere cannot be restored without fresh authorization.
QR generation is local. The pending secret is encrypted in the existing session, bound to the server, user and setup token, and expires after ten minutes or five failed attempts. The browser cannot supply a replacement seed. Recovery-code management is not included; use the server's normal recovery process.
Application passwords and aliases
Created app passwords inherit account permissions. Secrets are encrypted for one-time display with a two-minute expiry; listings never request secret hashes. Revocation accepts only listed app passwords, not the main password or API keys. Revoking Webmail's credential may require another login. Dates use midnight UTC.
Aliases are delivery addresses, not sender identities. Additions are limited to the account's primary domain, and only aliases are updated. Stale forms are rejected after re-reading the list. The API has no mutation-state precondition, so an external edit between that read and write cannot be completely excluded.
Public keys and masked email
Import one ASCII-armoured OpenPGP public key or PEM X.509 certificate, up to 16 KiB. Private-key armour and secret OpenPGP packets are rejected before API submission; Stalwart performs final cryptographic validation. Never upload private keys.
Import does not activate encryption-at-rest policy. Its selected key is labelled and cannot be deleted here while selected. Manage policy in the server's account portal and preserve private keys for existing encrypted mail. This page does not replace Enigma or another client-side decryption plugin.
Masked email requires Stalwart Enterprise. The server generates the address; the supplied site domain describes the website using it, not its delivery domain. Expiration is encoded in the address and cannot be extended. Expired addresses cannot be re-enabled. Addresses are not added automatically as sender identities. Key and masked-address lists show 20 entries per page.
Update, migrate or uninstall
Back up local configuration and review CHANGELOG.md. Select a new
tagged version with composer require evopixel/stalwart:<version>, or
use composer update evopixel/stalwart when following dev-main.
Retain your application's composer.lock and test before deploying. The installer
preserves local configuration; review new options in config.inc.php.dist.
Before uninstalling, restore a working password driver if needed, remove
stalwart from the enabled list, then run
composer remove evopixel/stalwart. Back up configuration you want to keep.
Uninstalling does not disable server 2FA, revoke passwords or remove server data.
No Roundcube database migration is needed. Temporary secrets use encrypted session
storage; remembered logins use encrypted records in the existing database cache.
Troubleshooting
| Symptom | Check |
|---|---|
| No settings entries | Plugin list, master/feature flags, URL, and local config overriding the main file |
| Wrong password provider | Password's own configuration, enabled plugin list, host/login restrictions |
| Missing QR | Composer dependencies and XMLWriter in the web PHP runtime, not only CLI |
| Connection/authentication error | Endpoint, TLS chain, outbound HTTPS, same-server credentials and OAuth refresh; never disable TLS validation |
| Unavailable alias or masked-email page | Required permissions; Enterprise license for masked email |
| TOTP setup refuses login method | Reconnect using an app password or OAuth |
| TOTP rejected | Clock synchronization, current six-digit code and unexpired setup session |
| App-password secret disappeared | Display is one-time; revoke an unused password and create another |
| Active key cannot be deleted | Change encryption policy through the account portal first |
Development and reporting
Use an isolated Roundcube installation with its Composer dependencies:
composer validate --strict
ROUNDCUBE_ROOT=/path/to/development/roundcube composer test
node --check stalwart.js
On PowerShell, set $env:ROUNDCUBE_ROOT = 'C:/path/to/development/roundcube' before
running composer test. When installed under Roundcube's plugins directory,
the tests discover the root automatically. Persistence checks require PDO SQLite
and an unchanged Persistent Login plugin in plugins/persistent_login. If it is
installed elsewhere, set PERSISTENT_LOGIN_ROOT to that plugin directory.
The API suite uses mock responses, synthetic keys and TOTP vectors. Lifecycle
checks use each version's real rcube_plugin class with test adapters. They check
feature gates, action registration, config precedence, menu order and resource
names. Browser fixtures use isolated API/IMAP services and SQLite.
Password-session checks simulate fresh authorization after a successful password
change, including TOTP, Remember me, identity checks and failure recovery.
This session-renewal path still needs an end-to-end check on a live Stalwart account.
Live Stalwart 0.16.26 checks cover password changes, TOTP enrollment/removal, application passwords, authorized alias changes, public keys, embedded login, OAuth with PKCE, token refresh, IMAP and SMTP. They use temporary accounts with stock Elastic and compatible custom skins. Enterprise masked-email operations still require validation on a licensed deployment; the Community rejection is covered. No production accounts or credentials are used.
Follow existing four-space PHP formatting and native hooks, actions, HTML helpers,
translations and the plugin template. Keep feature-specific helpers separate
and avoid core/skin patches. Keep translation fallbacks covered, preserve CSRF and
ownership checks, and add regression checks for security-sensitive changes.
See the Roundcube Plugin API.
Use GitHub issues for
sanitized bug reports. Report vulnerabilities privately through
GitHub security advisories. Never include credentials, tokens, cookies, TOTP seeds, private keys
or personal mail. Composer derives versions from Git; do not add a fixed version
field. Publish future fixes under new tags rather than replacing released tags.
License and attribution
Copyright (C) 2026 EvoPixel. GNU General Public License version 3 or later; see LICENSE.
Password-policy and session code in stalwart_password.php is adapted from
Roundcube's Password plugin:
Copyright (C) The Roundcube Dev Team; original author Aleksander Machniak
alec@alec.pl; GPL-3.0-or-later. Original notices remain in the adapted source.
Composer dependencies retain their own licenses and are not bundled. Public test keys are synthetic fixtures. This independent plugin is not an official Roundcube or Stalwart product and includes neither Stalwart server code nor an Enterprise license.