VacationResponse JMAP API: isEnabled reset to false whenever properties are changed

Issue Description

When modifying a VacationResponse using a JMAP VacationResponse/set modify operation, the isEnabled attribute is reset to false despite being untouched by the patch.

I cannot find anything in the specification that would warrant that behaviour.

Tested against v0.16.12-alpine

Expected Behavior

When modifying properties of the VacationResponse singleton that are not isEnabled, it should not modify isEnabled.

Actual Behavior

When I first send a VacationResponse/set that sets isEnabled to true, and query it, it is correctly set to true.
But when I then perform another VacationResponse/set that sets fromDate (and only that), and I query using VacationResponse/get, then the isEnabled attribute is set to false.

Reproduction Steps

  1. Set isEnabled to true:
{
    "using": [
        "urn:ietf:params:jmap:core",
        "urn:ietf:params:jmap:vacationresponse"
    ],
    "methodCalls": [
        [
            "VacationResponse/set",
            {
                "accountId": "e",
                "update": {
                    "singleton": {
                        "isEnabled": true
                    }
                }
            },
            "0"
        ]
    ]
}
  1. Get the current VacationResponse:
{
    "using": [
        "urn:ietf:params:jmap:core",
        "urn:ietf:params:jmap:vacationresponse"
    ],
    "methodCalls": [
        [
            "VacationResponse/get",
            {
                "accountId": "e"
            },
            "0"
        ]
    ]
}
  1. See that the result, indeed, has isEnabled set to true:
{
    "methodResponses": [
        [
            "VacationResponse/get",
            {
                "accountId": "e",
                "state": "sae",
                "list": [
                    {
                        "id": "singleton",
                        "isEnabled": true,
                        "fromDate": null,
                        "toDate": null,
                        "subject": null,
                        "textBody": null,
                        "htmlBody": null
                    }
                ],
                "notFound": []
            },
            "0"
        ]
    ],
    "sessionState": "e9f3d4ed"
}
  1. Modify the fromDate attribute only:
{
    "using": [
        "urn:ietf:params:jmap:core",
        "urn:ietf:params:jmap:vacationresponse"
    ],
    "methodCalls": [
        [
            "VacationResponse/set",
            {
                "accountId": "e",
                "update": {
                    "singleton": {
                        "fromDate": "2026-09-10T01:02:03Z"
                    }
                }
            },
            "0"
        ]
    ]
}
  1. Retrieve the current VacationResponse again and see that, now, the fromDate property has indeed been modified as expected, but the isEnabled property has been set to false:
{
    "methodResponses": [
        [
            "VacationResponse/get",
            {
                "accountId": "e",
                "state": "sai",
                "list": [
                    {
                        "id": "singleton",
                        "isEnabled": false,
                        "fromDate": "2026-09-10T01:02:03Z",
                        "toDate": null,
                        "subject": null,
                        "textBody": null,
                        "htmlBody": null
                    }
                ],
                "notFound": []
            },
            "0"
        ]
    ],
    "sessionState": "e9f3d4ed"
}

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

LDAP

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

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

This is a confirmed bug in the server’s VacationResponse/set handler, not intended JMAP behavior. Looking at the update logic, is_active is initialized to false at the start of processing each update and is only set to true if the request explicitly includes isEnabled: true. If isEnabled is omitted from the patch (as in your fromDate-only update), is_active stays false, and the server compares that against the currently active script to decide whether to deactivate it — effectively resetting isEnabled to false any time it isn’t explicitly re-sent.

The relevant code is in crates/jmap/src/vacation/set.rs, in the vacation_response_set update loop:

let mut is_active = false;
...
(Key::Property(VacationResponseProperty::IsEnabled), Value::Bool(value)) => {
    is_active = value;
}
(Key::Property(VacationResponseProperty::IsEnabled), Value::Null) => {
    is_active = false;
}
...
// later:
let was_active = active_script_id == Some(document_id);
if is_active {
    ...
} else if was_active {
    // clears ActiveScriptId, disabling vacation response
}

Because is_active should be seeded from the existing state (was_active) rather than defaulting to false when the property isn’t present in the patch, any partial update that omits isEnabled silently disables the vacation responder. This matches JMAP’s general “omitted properties are left unchanged” semantics being violated here.

As a workaround until this is fixed, always include isEnabled: true explicitly in every VacationResponse/set update call alongside whatever other properties you’re changing.

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

Fixed in v0.16.13 which will be released this week.