The Long Road to a Working Cyrus JMAP Server in 2026
My journey from Dovecot to Cyrus to get the JMAP experience
PART 2: July, Where I Started Dealing With Walls
Part 1 of this series ended with: “Then July arrived, and with that the actual road to a working JMAP server (Cyrus).”
You will recall that by then I had a Debian 13 server with Dovecot (ports 143, 993, 4190); and alongside a Cyrus IMAP server (ports 1143, 1993, 14190). I had SASL login working for one admin user via its local sasldb.
Now came by far the longest and hardest struggle of this journey. But before we run off into the adventures I encountered, let me explain the issue.
Postfixadmin - And how to make it work with Cyrus
Postfixadmin manages account metadata: Domains, mailbox addresses, aliases, quotas, and password hashes. In my case, it keeps this in a MariaDB database. It does not hold the emails itself, which Dovecot stores on disk in whatever format you choose (Maildir, mdbox, what-have-you).
So, when I want to keep Postfixadmin, I really want to keep the following two things:
- The ability to manage everything (domains, mailboxes (user@domain), aliases, quotas) in postfixadmin’s web UI, just like I have been doing until now. And then Cyrus just “magically” provisions those mailboxes on demand, just like Dovecot does.
- Cyrus has to authenticate against this MariaDB account database, so that I won’t have to have two user databases and keep them in sync and all that.
How Cyrus conjures mailboxes from a database it doesn’t own is its own story and the crux to make this work. See the “split-brain” explanation below.
Getting there meant working through three different auth backends.
Attempt one: sasldb
We will skip over the first attempt (end of June) with Cyrus’ own password file. I do not want to have two different databases, so this option is out. As a side note: I created a user ’testuser’ in the Cyrus database, and was confused to find out that I was getting 401 errors when trying to authenticate. Turns out that Cyrus adds the hostname as the realm (Cyrus’ word for the domain-part of an address) if you don’t provide a realm.
Attempt two: SASL auxprop straight at the MariaDB database
By pointing SASL directly at the MariaDB database, via its auxprop plugin, I reckoned I had it all sorted. However, before I could celebrate, I had a couple of hurdles to pass. I encountered:
sql_select option missing. It turns out that the SASL SQL plugin wants its parameters without thesasl_prefix in some contexts, and with it in others.fatal error: SQL engine mysql not supported. This one had me quite confused, and warrants a longer explanation.
The error sounds like Cyrus can’t use MySQL at all — not great, when wiring Cyrus to MariaDB is the entire point. But it turns out that that is not what was going on. There are two completely separate “SQL” layers in Cyrus, and I had accidentally crossed them.
The one is Cyrus’s own cyrusdb layer: Cyrus keeps its operational state — the mailbox list (mailboxes.db), per-user \Seen flags, subscriptions, annotations, the TLS-session and duplicate-delivery caches — in small embedded key-value files, each with a *_db backend setting (flat; skiplist; or the crash-safe twoskip – that’s the modern default). cyrusdb nominally has an sql backend too, driven by a global sql_engine.
The other, unrelated layer is the SASL sql auxprop plugin, driven by sasl_sql_*. While trying to wire SASL’s auxprop up to MariaDB, I had inadvertently set a global sql_engine: mysql, and Cyrus’s cyrusdb layer happily went with that and attempted to open its own internal databases as if they were MySQL databases. That was not going to work.
So, Cyrus’ internal state stays on the local file backends (twoskip); only the authentication layer talks to SQL. With that, Cyrus would start again.
Now that Cyrus was talking to the MariaDB database, and that database was already filled with data from my mail server, it should be pretty straightforward to get authenticated!
Well, here I hit a wall that was the dead end for this whole approach: With the connection now all good, it turned out that SASL auxprop’s sql plugin chokes on the Postfixadmin way of storing passwords as {scheme}-prefixed encrypted hashes. The plugin is happy to SELECT a stored hash, but it can’t verify a login against a {scheme}-prefixed encrypted hash. What next?
Attempt three: saslauthd -> PAM -> MariaDB (this one works!)
PAM has a plugin pam_mysql that can talk to the MariaDB database. It lets the system’s passphrase hasher crypt(3) verify the hashes, and crypt does not choke on the $2y$... bcrypt-hashes (or whatever you have Postfixadmin generate).
We can chain this all together: Cyrus -> saslauthd -> PAM -> MariaDB.
Let’s call this the “auth chain”.
Cyrus uses saslauthd, which talks to PAM. PAM’s pam_mysql talks to the database and defers the hash comparison to the system’s crypt. It works, and it lets me use Postfixadmin with Cyrus!
PAM needs to be configured to talk to the database (see the Cyrus JMAP Howto for details), and it uses different configuration files which are named after the service name that is called. As a result, I configured /etc/pam.d/imap and created a softlink from /etc/pam.d/HTTP and one from /etc/pam.d/smtp to it. That way the configuration files can’t drift apart.
Note
NOTE: If you follow this setup, then your pam_mysql.so layer is going to determine which password schemes are supported.
I ran into one issue that I want to mention here. Now that authentication was mostly working, except sometimes it wasn’t. Sometimes I would see just the bare username (no domain) in the logs. My setup needs full user@domain.tld for authentication (I serve multiple domains), and testsaslauthd succeeded with full addresses. What was stripping the domain part off some logins? It turned out to be the defaultdomain setting. If you configure the defaultdomain, Cyrus seems to think that it can strip the domain part, which caused password checks to fail. I removed the entire defaultdomain from /etc/imapd.conf and all was good again.
Writing about serving multiple domains: If you do that, you must list every hosted domain in /etc/imapd.conf under loginrealms, or they can’t log in.
Split brain
Let me end this blog post with what I call the “split brain”. This is what it comes down to:
- postfixadmin’s SQL drives Postfix (which domains it accepts; which recipients are valid; all aliases/forwards), and it is the credential store that Cyrus authenticates against via
pam_mysql. - Cyrus owns the actual mailboxes and speaks IMAP/JMAP (and other things like calendars, contacts, news, …).
They are glued together by two things: The “auth chain” (see: above), and Cyrus’s autocreate. These are the settings that I configured:
# Auto-create the inbox when a user logs in or a mail arrives via LMTP
autocreate_users: anyone
autocreate_post: 1
autocreate_quota: 512000
autocreate_inbox_folders: Sent | Drafts | Trash | Junk | Archive
autocreate_subscribe_folders: Sent | Drafts | Trash | Junk | Archive
See the imapd.conf man page for more information on these (and other) settings.
In practise:
- Add a mailbox in postfixadmin -> the credentials now exist in the MariaDB database.
- Postfix routes to it.
- Cyrus auto-creates the mailbox on first use.
No need for any CLI commands and cyradm and what-not. Nothing against them, but, as stated earlier, I don’t use these often enough to be able to remember them; and then it becomes a hassle.
The one thing that does not work still is adding a new domain. A new domain still needs to be added to /etc/imapd.conf under loginrealms before users of that domain can log in. I added a script to my system to take care of this, which can be found in the Cyrus JMAP Howto article.
This was a lot of work just to keep Postfixadmin going. Honestly, it wasn’t written for it, which is why it’s not straightforward to set up. This made me wonder what the alternatives are, because, surely, no admin is going to be adding users and mailboxes and quotas and everything by hand via the cli?
A search online revealed that the following options exist:
cyradm+ scripts. All on the command line. Sure, but not ideal for someone who doesn’t do this much. A small server is set up and forget, and every now and then add or remove something.- LDAP + a web UI (e.g. LDAP Account Manager). Users live in the LDAP directory. Cyrus auto-provisions off the LDAP mail attributes. Postfix routes via LDAP, too. Jolly good for enterprises, but not for a small setup.
- Turnkey suites like Kolab or Univention UCS. They bundle Cyrus + LDAP + a web UI and much more into a nice big suite. Fancy if that’s what you’re looking for, but if it is just mail (and perhaps calendars and contacts), then that’s overkill.
The lack of a well-integrated or easy-to-set-up Cyrus-native or Cyrus-compatible admin GUI is perhaps one of the reasons why Cyrus is not very popular among hobbyists and small mailserver owners. Postfixadmin is a good web admin, and if we can make it work for Cyrus, that’s a great start (I hope)!
That wraps it up for Part 2. There are still a couple of parts coming, not because the end result is so hard to set up, but because I just poked and blundered along, trying to get it running. That is what this series of posts relates, and why they are lengthy. But rest assured, it has a pretty good ending with a running Cyrus JMAP server, reverse proxy, webmail, and it works on mobile, too!