V0.15.4 to v0.16.4 Upgrading Issues

Your question

So, I attempted the migration. My ubuntu server is binary installation, no docker etc, have been using for almost a year now. (I think initially v0.14).

Anyway, here’s the breakdown of errors I got:

I encountered the "error: redis store missing required 'urls'" that I saw someone mentioning above. The only fix that worked was the following:

@mdecimus Sorry I should've been more clear that I was referring to a solution for the redis issue mentioned. With the current migrate_v016.py I still encounter error: redis store missing required 'urls' but this seemed to resolve it?

$ cat migrate_v016_redis.patch
--- migrate_v016.py
+++ migrate_v016.py
@@ -1319,8 +1319,14 @@
             body["depth"] = depth
         return body

-    def _build_redis_single(self, sub: dict[str, str]) -> dict[str, Any]:
-        urls = collect_array(sub, "urls")
+    def _build_redis_single(self, sub: dict[str, Any]) -> dict[str, Any]:
+        raw_urls = sub.get("urls")
+        if isinstance(raw_urls, str):
+            urls = [raw_urls]
+        elif isinstance(raw_urls, list):
+            urls = raw_urls
+        else:
+            urls = []
         if not urls:
             raise ConvertError("redis store missing required 'urls'")
         body: dict[str, Any] = {"@type": "Redis", "url": urls[0]}

Not python fluent, but I get that it contains redis data.
Question 1: Are they critical data? Because if they are in redis (as in, cache), can I safely ignore them, not even include them in the migration, and the server will naturally generate them as it goes?

Problem 2: After fixing the above script, everything went smoothly, or so I thought. It showed me some “unmigrated.txt” entries about stuff that “should be reviewed manually”. Okay, fair enough. Continued with the steps, got the service working, server was up, accounts existing. Then I realized, that the unmigrated entries was upwards of 5000.

Upon inspection, it was mostly banned IPs and spam filter entries (I think?). It had a bunch of IPs and domains. My problem with this is that there were about 50-100 settings that were not deligated to the settings.json file. Data stores for traces/analytics, response headers (more on that in a bit), and even redis datastore settings.

The biggest problem, was that I use bulwark webmail. The webmail created for this server. The problem is:
CORS… CORS… CORS… When I used v0.15, I could inject response headers from the Settings → HTTP → Settings in the stalwart web UI. E.g.:

Access-Control-Allow-Origin: htt.ps://webmail.example.com
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS,PATCH
Access-Control-Allow-Headers: Content-Type, Authorization, X-JMAP-Request-Id, X-Requested-With

Using these 3 headers, stalwart told my browser that htt.ps://webmail.example.com was trusted domain. BUT, with v0.16, HTTP API is removed. Bulwark (correctly and as expected) is using JMAP. Stalwart uses JMAP. Every and all communication between the 2 apps is by JMAP. But there is no way to inject JMAP Headers to say that the htt.ps://webmail.example.com is trusted. The only solution (I think) is to either use a proxy for stalwart, OR have bulwark under a subdomain (using a proxy on bulwark).

Final note: I would sit down and do every unmigrated.txt setting some manual work and “translate” them by hand to the new UI, but having no webmail available is the ultimate blocking.

We need a setting in stalwart to either “trust CORS for a specific subdomain.domail.tld”, or allow us to inject response headers in the jmap. In my opinion, we should have a dedicated section along the lines of “Webmail Settings” that allows Stalwart to set CORS for that specific subdomain, and maybe even better rate limits for that service (since everyone is going THROUGH it?).

To be honest, I even thought about doing a fresh installation to another server so users have 0 downtime and slowly migrate accounts and even emails (last month or so), but in the end, without Webmail, v0.16 has become unusable.

Side note: Unfortunately, I restored the previous v0.15 version on my VM, and have no logs of the migration, but I am willing to re-do everything in order to help fix things if you want me to.

Some extra info: Even with permissive CORS for HTTP I couldn’t get it to work. (even though I would prefer to have them on specific subdomain instead of wildcard).

From my understanding, the error on Bulwark frontend is that JMAP is not using permissive CORS (not the HTTP).

I am willing to be corrected since it was well into an hour of console debugging on other stuff for the upgrade, and I was not in the right state of mind to correctly debug.

Let me research both issues - the Redis migration question and the CORS/response headers question.

Here are answers to each of your questions:


Question 1: Is the Redis data critical?

No, it is not critical to lose Redis data during migration. Redis is used by Stalwart exclusively as an in-memory store for caching, ephemeral key-value data, and cluster coordination (pub/sub for things like IMAP IDLE, blocked IP propagation, etc.). It is not a primary data store – no emails or account data live there. If the migration script can’t migrate your Redis config and you skip it, the server will simply start without a configured in-memory store, or you can re-add the Redis connection manually after the migration through the WebUI under Settings > Storage > In-Memory Store. The cache will repopulate naturally as the server runs.


Problem 2: CORS for Bulwark / JMAP

Good news: CORS is configurable in the current version, and it applies to all HTTP responses including JMAP. There are two approaches:

Option A – Permissive CORS (wildcard, simplest):
In the WebUI go to Network > HTTP > Security and enable usePermissiveCors. This sets Access-Control-Allow-Origin: * on all responses, which will unblock Bulwark immediately. The docs note you should ensure all sensitive endpoints remain protected by auth (which JMAP already does).

Option B – Custom Access-Control-Allow-Origin response header (specific origin):
Go to Network > HTTP > General and add entries under responseHeaders. This is the same feature you used in v0.15, just now configured through the unified HTTP object. For example:

{
  "responseHeaders": {
    "Access-Control-Allow-Origin": "https://webmail.example.com",
    "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,PATCH",
    "Access-Control-Allow-Headers": "Content-Type, Authorization, X-JMAP-Request-Id, X-Requested-With"
  }
}

These headers are returned with every HTTP response, including JMAP endpoints. This is the per-origin approach you were looking for and avoids the wildcard.

So the functionality you need is not gone – it just moved. The responseHeaders map on the Http singleton covers JMAP responses, and the usePermissiveCors flag is the quick toggle. Try Option B first for a specific-origin policy, and fall back to Option A if you run into preflight (OPTIONS) handling issues.

Relevant docs:

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

  1. Redis migration. Your patch is correct; the migration script assumed urls is always a list and falls over when 0.15 had it as a single string. I’ll fold the str-or-list handling into the script.

  2. Unmigrated entries. Most of what you’re seeing in unmigrated.txt is intentionally not migrated: banned IPs, throttle counters, spam-filter learning state, and the like are per-installation cache state that we explicitly don’t carry across major versions because the on-disk format and the model both shifted. Banned IPs in particular age out within hours; spam-filter learning will rebuild from new mail flow. You can safely ignore those entries. The ones worth a closer look are the response-header rules (see point 3) and any data-store settings (storage.*).

  3. The bot response is correct, you can still manually add headers to the response in v0.16.