Listing or installing Sieve scripts per JMAP fails (was working with v0.12)

Your question

I was using JMAP to list and update Sieve script(s) in Stalwart v0.12.

Now with v0.16 the identical code fails :frowning:

I’m sending the following request to list the existing Sieve scripts (the used account has none!): POST https://boulder.egroupware.org/jmap/

{
  "using" : [ "urn:ietf:params:jmap:mail" ],
  "methodCalls" : [ [ "SieveScript/get", {
    "accountId" : "b"
  }, "0" ] ]
}

I get the following response:

HTTP/2 400
Content-Type: application/problem+json

{"type":"urn:ietf:params:jmap:error:notRequest","status":400,"detail":"{\"using\":[\"urn:ietf:params:jmap:mail\"],\"methodCalls\":[[\"SieveScript\\/get\",{\"accountId\":\"b\"},\"0\"]]}"}

I thought this might be caused by no Sieve script exists and therefore is activated, so I tried installing one: POST https://boulder.egroupware.org/jmap/upload/b/ with the script as body and got the expected response:

{"accountId":"b","blobId":"ed00tvwkyeak7oobbmum9i0d1fbbjh3xsvj29gv2paa7gkupjolcaamt17kncbq","type":"application\/x-www-form-urlencoded","size":713}

Then I tried installing and activating it:

{
  "using":["urn:ietf:params:jmap:mail"],
  "methodCalls":[
    ["SieveScript/set",{
      "accountId":"b",
      "create":{
        "A":{
          "name":"mail",
          "blobId":"ed00tvwkyeak7oobbmum9i0d1fbbjh3xsvj29gv2paa7gkupjolcaamt17kncbq"
        }
       },
       "onSuccessActivateScript":"#A"
      },
      "0"
    ]
  ]
}

And again I get a similar error-response as when I tried listing the scripts:

HTTP/2 400
Content-Type: application/problem+json

{"type":"urn:ietf:params:jmap:error:notRequest","status":400,"detail":"{\"using\":[\"urn:ietf:params:jmap:mail\"],\"methodCalls\":[[\"SieveScript\\/set\",{\"accountId\":\"b\",\"create\":{\"A\":{\"name\":\"mail\",\"blobId\":\"ed00tvwkyeak7oobbmum9i0d1fbbjh3xsvj29gv2paa7gkupjolcaamt17kncbq\"}},\"onSuccessActivateScript\":\"#A\"},\"0\"]]}"}

I checked that “b” is in fact my JMAP accountId.
I have no idea what has changed between Stalwart v0.12 and v0.16 that causes the working JMAP code for Sieve to fail now :frowning:

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The error urn:ietf:params:jmap:error:notRequest with a 400 status means the server is rejecting the request before even processing the method calls - specifically, it cannot parse or validate the request envelope. The most likely cause is that your using array is missing the capability URI required for Sieve scripts.

The issue is clear. The using capability URI for JMAP Sieve has changed.

Your requests are declaring "using": ["urn:ietf:params:jmap:mail"], but SieveScript/get and SieveScript/set require the Sieve capability to be listed. The correct URI is urn:ietf:params:jmap:sieve (per RFC 9661, which Stalwart now implements). Stalwart returns a notRequest error when a method is called without its required capability declared in using.

Update your requests to include it:

{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:sieve"],
  "methodCalls": [["SieveScript/get", {"accountId": "b"}, "0"]]
}

And for the set/create:

{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:sieve"],
  "methodCalls": [
    ["SieveScript/set", {
      "accountId": "b",
      "create": {
        "A": {
          "name": "mail",
          "blobId": "ed00tvwkyeak7oobbmum9i0d1fbbjh3xsvj29gv2paa7gkupjolcaamt17kncbq"
        }
      },
      "onSuccessActivateScript": "#A"
    }, "0"]
  ]
}

This is consistent with how Stalwart advertises the urn:ietf:params:jmap:sieve capability in its session resource. Relevant docs: JMAP for Sieve.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Ok, sounds like a reason, so I changed my code as requested, and send:

{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:sieve"],
  "methodCalls": [["SieveScript/get", {"accountId": "b"}, "0"]]
}

But I still get the error-repsonse:

HTTP/2 400
Content-Type: application/problem+json

{"type":"urn:ietf:params:jmap:error:notRequest","status":400,"detail":"{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:ietf:params:jmap:sieve\"],\"methodCalls\":[[\"SieveScript\\/get\",{\"accountId\":\"b\"},\"0\"]]}"}

Same when I try installing a new script I uploaded to the blob-store:

{
  "type" : "urn:ietf:params:jmap:error:notRequest",
  "status" : 400,
  "detail" : "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:ietf:params:jmap:sieve\"],\"methodCalls\":[[\"SieveScript\\/set\",{\"accountId\":\"b\",\"create\":{\"A\":{\"name\":\"mail\",\"blobId\":\"ed09amsxpmdkki7rf77ovqrr7fsfjtcrc2xkpk372ytum7wcjvp2gapoqgk0cbq\"}},\"onSuccessActivateScript\":\"#A\"},\"0\"]]}"
}

Seems like a bug to me :wink:

Here’s the Docker logs on level trace:

2026-06-07T09:01:27Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 172.18.0.16, remotePort = 41854, details = [["host", "boulder.egroupware.org"], ["connection", "Upgrade"], ["x-real-ip", "172.18.0.17"], ["x-forwarded-for", "172.18.0.17"], ["content-length", "127"], ["content-type", "application/json"], ["authorization", "Basic cmFsZkBib3VsZGVyLmVncm91cHdhcmUub3JnOnJhbGJlYzMyd2ch"], ["accept", "application/json"]], contents = "{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:sieve"],"methodCalls":[["SieveScript\\/get",{"accountId":"b"},"0"]]}", size = 127
2026-06-07T09:01:27Z DEBUG JMAP input is not a request (jmap.not-request) listenerId = "https", localPort = 443, remoteIp = 172.18.0.16, remotePort = 41854, reason = "invalid type: string "SieveScript/get", expected a borrowed string at line 1 column 102", details = "{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:sieve"],"methodCalls":[["SieveScript\\/get",{"accountId":"b"},"0"]]}"
2026-06-07T09:01:27Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 172.18.0.16, remotePort = 41854, contents = "{"type":"urn:ietf:params:jmap:error:notRequest","status":400,"detail":"{\\"using\\":[\\"urn:ietf:params:jmap:core\\",\\"urn:ietf:params:jmap:sieve\\"],\\"methodCalls\\":[[\\"SieveScript\\\\/get\\",{\\"accountId\\":\\"b\\"},\\"0\\"]]}"}", code = 400, size = 217

Maybe that helps diagnosing the problem.

Ralf

Ok, figured out what the problem is, it’s PHP’s json_decode() by default escapes slashes as e.g. in “SieveScript/get” with a backslash, which Stalwart does not recognize as escaped slash and therefore returns the error :frowning:

cat <<EOF | curl -i https://boulder.egroupware.org/jmap/ --user '[email protected]' --data @- -H "Content-Type: application/json"
{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:sieve"],
  "methodCalls": [["SieveScript/get", {"accountId": "b"}, "0"]]
}
EOF
HTTP/2 200
server: nginx/1.30.2
date: Mon, 08 Jun 2026 14:38:53 GMT
content-type: application/json; charset=utf-8
content-length: 125
x-content-type-options: nosniff

{"methodResponses":[["SieveScript/get",{"accountId":"b","state":"n","list":[],"notFound":[]},"0"]],"sessionState":"3b84d638"}

cat <<EOF | curl -i https://boulder.egroupware.org/jmap/ --user '[email protected]' --data @- -H "Content-Type: application/json"
{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:sieve"],
  "methodCalls": [["SieveScript\/get", {"accountId": "b"}, "0"]]
}
EOF
HTTP/2 400
server: nginx/1.30.2
date: Mon, 08 Jun 2026 14:47:28 GMT
content-type: application/problem+json
content-length: 223

{"type":"urn:ietf:params:jmap:error:notRequest","status":400,"detail":"{\"using\": [\"urn:ietf:params:jmap:core\", \"urn:ietf:params:jmap:sieve\"],\"methodCalls\": [[\"SieveScript\\/get\", {\"accountId\": \"b\"}, \"0\"]]}"}

So JSON for Stalwart v0.16 needs to be encoded with PHP’s
json_encode($json, PHP_UNESCAPED_SLASHES)

The spec says you can escape them, but you don’t have to …

Ralf