Platform ·DarDev Team · 5 min read

Server directory in webmail via JMAP principals

The stalwart_directory Roundcube plugin queries Stalwart JMAP Principal/query so compose autocomplete lists every hosted mailbox across send and sales domains.

Compose autocomplete showing Stalwart directory contacts in Roundcube

The stalwart_directory Roundcube plugin gives compose autocomplete a read-only view of every mailbox Stalwart hosts—across send.dardev.net, sales.dardev.net, and future outbound domains—without manually maintaining a shared address book. It queries Stalwart JMAP Principal/query over the internal Docker network and caches results for five minutes. Personal CardDAV books are separate; this article covers the server directory only.

Staff typing in the To field see colleagues and role accounts—news@send.dardev.net, contact@send.dardev.net, sales mailboxes—as they type. The plugin is custom DarDev code under roundcube/plugins/stalwart_directory/. For personal contacts sync, read carddav-personal-vs-server-directory. For webmail stack context, see inside-unified-mailer-stack and roundcube-stalwart-self-hosted-webmail.

Why JMAP principals instead of LDAP

Stalwart already exposes account metadata through JMAP. Rather than add LDAP or duplicate CSV exports into Roundcube SQL tables, we implemented a thin rcube_addressbook backend that calls Principal/query at stalwart_directory_jmap_url—default http://stalwart:8080/jmap on the mailer network. Principals return email, display name, and description fields mapped to Roundcube contact records.

The backend is read-only: no create, update, or delete. Account lifecycle stays in Stalwart WebAdmin or infrastructure scripts like create-contact-mailbox.sh. That boundary prevents webmail users from accidentally corrupting the authoritative directory.

Plugin structure

  • stalwart_directory.php — registers addressbook source stalwart_directory on init
  • stalwart_directory_backend.php — extends rcube_addressbook with search, list, count
  • Static principal_cache — 300 second TTL to avoid hammering JMAP on every keystroke
  • Config stalwart_directory_jmap_url in custom.inc.php

Autocomplete wiring

roundcube/config/custom.inc.php sets autocomplete_addressbooks to stalwart_directory and sql, with default_addressbook stalwart_directory. On compose, Roundcube merges matches from the server directory first, then SQL contacts if any. The carddav plugin appends carddav_* source ids after login for personal books—documented separately.

Search behavior

matching_principals filters the cached principal list by substring on email and description fields. list_records and search implement the Roundcube addressbook interface so the standard compose UI works without forked JavaScript. count returns match cardinality for pagination UI. set_search_set and reset follow Roundcube lifecycle for repeated queries in one compose session.

If autocomplete returns empty, check Stalwart has accounts with JMAP principal entries, verify stalwart_directory_jmap_url from inside the Roundcube container, and run scripts/mail/probe-stalwart-directory-list.php or probe-stalwart-directory-search.php on the VPS for structured debug output—no secrets printed.

Export alignment

bash scripts/mail/export-webmail-contacts.sh exports the same principal universe to CSV for ops or CRM enrichment—default path uses JMAP principals, with --carddav flag for personal books only. Keeping export and autocomplete on the same Stalwart API reduces drift between what staff see in webmail and what scripts ingest offline.

Security and performance

  • JMAP URL is HTTP on internal Docker network only—not published on host 443
  • Read-only backend—no principal mutation from webmail
  • Five-minute cache balances freshness vs JMAP load during busy compose sessions
  • Plugin loads only for authenticated Roundcube sessions—no anonymous directory leak

The plugin ships in the custom dardev/roundcube image built from roundcube/Dockerfile alongside carddav and standard Roundcube plugins. Deploy updates with scripts/deploy-webmail.sh. This pattern—small custom glue on standard OSS—matches how we extend the company platform in multi-product-saas-one-core without forking upstream mail servers.

principal_to_contact mapping

Each JMAP principal row becomes a Roundcube contact with generated ID, email in primary email field, display name from description or local-part, and source tag stalwart_directory. Groups are disabled—Stalwart principals are flat mailboxes today. undelete and write paths intentionally no-op so Roundcube UI hides edit buttons for directory entries.

When to choose directory over SQL contacts

Roundcube sql addressbook still exists for legacy local contacts. We default to directory because Stalwart provisions accounts centrally—SQL imports would duplicate principals and stale quickly. If a team needs a static external mailing list not in Stalwart, use CardDAV personal book or CRM export instead of forking directory code.

Future extensions

Possible v2 improvements: shorter cache TTL toggle via config, domain-scoped principal filter for large multi-tenant hosts, and group expansion if Stalwart exposes mailing lists as principals. DarDev single-VPS scale does not require those yet. Monitor JMAP latency in platform health collectors alongside roundcube container restarts.

For deployed-realities-explained honesty: directory autocomplete shipped with Roundcube cutover; it is production for internal staff, not a roadmap slide. Test with probe scripts after every Stalwart account bulk import from school or services pipelines.

Relationship to Stalwart WebAdmin

WebAdmin at mta.dardev.net remains the authority for create, disable, and password reset. Directory plugin reflects WebAdmin state on next cache refresh—it is not a second admin UI. When ops provisions contact@send.dardev.net via create-contact-mailbox.sh, directory picks up the principal without Roundcube restart. Disabled accounts should disappear from autocomplete after cache TTL; if not, verify JMAP still lists the principal as active.

Compose autocomplete is the primary user-visible win. Address book browsing under Contacts may still emphasize CardDAV personal books—staff who open Contacts first should know directory entries appear primarily when addressing mail, not always as a browsable folder tree.

Does stalwart_directory replace CardDAV?

No. Directory is read-only all-mailboxes autocomplete. CardDAV syncs personal editable address books. Use both.

Why is my colleague missing from autocomplete?

Confirm the account exists in Stalwart WebAdmin with a principal email. Wait up to five minutes for cache expiry or restart Roundcube after bulk account imports.

Can webmail users edit the server directory?

No. stalwart_directory_backend sets readonly true. Edits happen in Stalwart admin or provisioning scripts.

What JMAP endpoint does the plugin call?

Principal/query against stalwart_directory_jmap_url—default http://stalwart:8080/jmap inside Docker.

How do I debug empty directory results?

Run scripts/mail/probe-stalwart-directory-list.php on the VPS and scripts/mail/validate-webmail.sh for container connectivity.

Get company news

Releases and announcements — confirm from your inbox.

Subscribe to updates