Please help... updating aliases

Your question

sreutter
33m
Your question

== Erstelle Account ==
Created Account bl
RAW RESPONSE:
bl
error: invalidPatch
Invalid key for object property
Properties: aliases
error: update failed

this error comes from this code:

ACCOUNT_ID=$(stalwart-cli --url “$URL” --api-key “$API_KEY”
query Account
–where “name=$NAME”
–fields id
–json
| jq -r ‘.id’)

echo “RAW RESPONSE:”
echo “$ACCOUNT_ID”

stalwart-cli --url “$URL” --api-key “$API_KEY”
update Account “$ACCOUNT_ID”
–field “aliases=$ALIASES_JSON”

Let me look up how aliases are managed in Stalwart.

The issue is clear. The error invalidPatch - Invalid key for object property - Properties: aliases means you are trying to set aliases directly as a top-level patch key during an update, but aliases is a list of nested EmailAlias objects and must be patched using JMAP’s pointer syntax, not as a plain key replacement.

The aliases field is a list of EmailAlias objects, each with these required fields:

  • name - the local part (before @)
  • domainId - the ID of the domain object
  • enabled - boolean (defaults to true)
  • description - optional string

For create, you pass aliases as an array of those objects:

"aliases": [
  {
    "name": "bob",
    "domainId": "<Domain id>",
    "enabled": true
  }
]

For update, you use JMAP patch pointer syntax to target individual list items or replace the whole list. To replace the entire aliases list on an existing account:

"update": {
  "<account-id>": {
    "aliases": [
      {
        "name": "bob",
        "domainId": "<Domain id>",
        "enabled": true
      }
    ]
  }
}

The most common mistake is passing aliases as a plain string, a plain object {}, or using an incorrect key format. Make sure you are passing it as a JSON array of EmailAlias objects with at least name and domainId present. If you share the actual code/payload you are sending, the community can pinpoint the exact issue.

Reference: Account object docs

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

Please stop creating dozens of topics with incomplete problems descriptions.

I’m sorry for the inconvenience, but I haven’t created dozens of threads – just three. And I only did that because I felt the Stalwart Help bot was giving me better answers than the AI I usually use. So I thought it was a bit unfair to mute me after just three posts. I’ll post the working script here now. Please note that if ‘postmaster’ and ‘abuse’ are already set as aliases, the script will naturally return an error.

#!/usr/bin/env bash
set -euo pipefail

Approach 2: Two-stage - create account first, then set aliases via update.

The account ID is read DIRECTLY from the create output (“Created Account ”)

instead of using a query. This guarantees that exactly one ID is present

 → no “bo\nba”, no HTTP 400.

CLI=“stalwart-cli”
URL=“https://example.com”
API_KEY=“API....”

echo “== Loading domains ==”

DOMAINS_OUTPUT=$(
$CLI --url “$URL” 
–api-key “$API_KEY” 
query Domain
)

echo “”
echo “Available domains:”
echo “--------------------”
echo “$DOMAINS_OUTPUT”
echo “”

read -rp "Account Name (name): " NAME
read -rp "Full Name: " FULL_NAME
read -rp "Domain ID: " DOMAIN_ID
read -rp "Aliases (comma-separated, empty = none): " ALIASES_INPUT

Build aliases as index-keyed map: {“0”:{…},“1”:{…}}

(Stalwart expects these collections as a map, NOT as an array - just like credentials.)

IFS=‘,’ read -ra ALIASES_ARRAY <<< “$ALIASES_INPUT”

ALIASES_JSON=“{”
idx=0
first=true
for alias in “${ALIASES_ARRAY[@]}”; do
alias=$(echo “$alias” | xargs)
[[ -z “$alias” ]] && continue

if [[ "$first" == true ]]; then
    first=false
else
    ALIASES_JSON+=","
fi

ALIASES_JSON+="\"$idx\":{\"name\":\"$alias\",\"domainId\":\"$DOMAIN_ID\",\"enabled\":true}"
idx=$((idx + 1))

done
ALIASES_JSON+=“}”

echo “Aliases JSON:”
echo “$ALIASES_JSON”

echo “== Creating account ==”

CREATE_OUTPUT=$(
$CLI --url “$URL” 
–api-key “$API_KEY” 
create Account/User 
–field “name=$NAME” 
–field “description=$FULL_NAME” 
–field “domainId=$DOMAIN_ID” 
–field ‘credentials={“0”:{“@type”:“Password”,“secret”:“AnfangsPasswort”}}’ 
–field ‘memberGroupIds={}’ 
–field ‘roles={“@type”:“User”}’ 
–field ‘permissions={“@type”:“Inherit”}’ 
–field ‘locale=de_DE’ 
–field ‘timeZone=Europe/Berlin’ 
–field ‘quotas={“maxDiskQuota”:524288000}’ 
–field ‘aliases={}’ 
–field ‘encryptionAtRest={“@type”:“Disabled”}’
)

echo “$CREATE_OUTPUT”

Extract ID from “Created Account ” → guaranteed to be a single value.

ACCOUNT_ID=$(echo “$CREATE_OUTPUT” | awk ‘/Created Account/ {print $NF; exit}’)

if [[ -z “$ACCOUNT_ID” ]]; then
echo “ERROR: Could not read account ID from create output.” >&2
exit 1
fi

echo “Account ID: $ACCOUNT_ID”

Only set aliases if any were provided

if [[ “$ALIASES_JSON” != “{}” ]]; then
echo “== Setting aliases ==”
$CLI --url “$URL” --api-key “$API_KEY” 
update Account “$ACCOUNT_ID” 
–field “aliases=$ALIASES_JSON”
echo “Aliases set.”
else
echo “No aliases provided - update skipped.”
fi

I hope this helps people who want to do the same.
this is the solution to the problem.

Here is a even more improved script, with:
-duplicate checking
-automatic password generation

#!/usr/bin/env bash
set -euo pipefail

v3: like v2 (two-step process: create account, then add aliases via update),

BUT with a pre-check:

- Does the account (name@domain) already exist?

- Does one of the requested aliases already exist (as an account name

OR as an alias of another account, domain-specific in each case)?

If there are conflicts, the input can be corrected before anything

is actually created (or the process can be aborted).



Extension:

- Automatic password generation

- Password is used directly during account creation

- Password is displayed at the end

CLI=“stalwart-cli”
URL=“example.com”
API_KEY=“API_.....”

Convenient wrapper so URL/key do not have to be repeated everywhere.

scli() { “$CLI” --url “$URL” --api-key “$API_KEY” “$@”; }

---------------------------------------------------------------------------

0) Generate password

---------------------------------------------------------------------------

generate_password() {
local length=“${1:-24}”

if command -v openssl >/dev/null 2>&1; then
    # Generates a suitable password without problematic shell/JSON characters.
    openssl rand -base64 48 | tr -dc 'A-Za-z0-9_@%+=-' | head -c "$length"
else
    # Fallback without openssl.
    tr -dc 'A-Za-z0-9_@%+=-' < /dev/urandom | head -c "$length"
fi

}

PASSWORD=“$(generate_password 24)”

if [[ -z “$PASSWORD” ]]; then
echo “ERROR: Password could not be generated.” >&2
exit 1
fi

---------------------------------------------------------------------------

1) Show domains (selection help) + valid domain IDs for validation

---------------------------------------------------------------------------

echo “== Loading domains ==”
DOMAINS_OUTPUT=$(scli query Domain)

echo “”
echo “Available domains:”
echo “--------------------”
echo “$DOMAINS_OUTPUT”
echo “”

Fetch valid domain IDs separately as JSON (for input validation).

If this fails, validation is silently skipped.

DOMAIN_IDS=$(scli query Domain --fields id --json 2>/dev/null | jq -r ‘.id’ 2>/dev/null || true)

---------------------------------------------------------------------------

2) Load existing accounts + aliases ONCE (for duplicate checking)

Result: OCCUPIED_TSV with lines “localpartdomainIdtypeaccount”

---------------------------------------------------------------------------

echo “== Loading existing accounts (for duplicate check) ==”
OCCUPIED_TSV=“”
ACCOUNT_IDS=$(scli query Account --fields id --json 2>/dev/null | jq -r ‘.id’ 2>/dev/null || true)

if [[ -n “$ACCOUNT_IDS” ]]; then
while IFS= read -r aid; do
[[ -z “$aid” ]] && continue
obj=$(scli get Account “$aid” --json 2>/dev/null || true)
[[ -z “$obj” ]] && continue
# .aliases
? covers array, map, and null forms.
lines=$(jq -r ’
(.name)     as $acct
| (.domainId) as $dom
| “($acct)\t($dom)\tAccount\t($acct)”,
(.aliases
? | “(.name)\t(.domainId)\tAlias\t($acct)”)
’ <<< “$obj” 2>/dev/null || true)
[[ -n “$lines” ]] && OCCUPIED_TSV+=“$lines”$‘\n’
done <<< “$ACCOUNT_IDS”
fi

Helper: is localpart@domainId already taken?

addr_taken() {
awk -F’\t’ -v lp=“$1” -v d=“$2” ‘$1==lp && $2==d {f=1} END{exit !f}’ <<< “$OCCUPIED_TSV”
}

Helper: print occupying entries in a human-readable format

addr_info() {
awk -F’\t’ -v lp=“$1” -v d=“$2” 
‘$1==lp && $2==d {printf "      - %s (Domain ID %s): already used as %s on account "%s"\n", $1, $2, $3, $4}’ 
<<< “$OCCUPIED_TSV”
}

---------------------------------------------------------------------------

3) Input loop: first collect + check, only create when conflict-free

---------------------------------------------------------------------------

while true; do
echo “”
read -rp "Account name (name): " NAME
read -rp "Full name: " FULL_NAME
read -rp "Domain ID: " DOMAIN_ID
read -rp "Aliases (comma-separated, empty = none): " ALIASES_INPUT

# Validate domain ID (if the list could be loaded)
if [[ -n "$DOMAIN_IDS" ]] && ! grep -Fxq "$DOMAIN_ID" <<< "$DOMAIN_IDS"; then
    echo "ERROR: Domain ID '$DOMAIN_ID' is not in the list of domains." >&2
    echo "Please enter it again."
    continue
fi

# Read aliases + normalize them (trim, remove empty values, remove duplicates)
IFS=',' read -ra _RAW_ALIASES <<< "$ALIASES_INPUT"
ALIASES=()
for a in "${_RAW_ALIASES[@]}"; do
    a=$(echo "$a" | xargs)
    [[ -z "$a" ]] && continue

    # Duplicate within the input itself?
    skip=false
    for existing in "${ALIASES[@]:-}"; do
        [[ "$a" == "$existing" ]] && skip=true && break
    done
    $skip || ALIASES+=("$a")
done

# --- Collect conflicts ---
CONFLICTS=()

# a) Account itself
if addr_taken "$NAME" "$DOMAIN_ID"; then
    CONFLICTS+=("Account '$NAME' already exists in this domain:")
    CONFLICTS+=("$(addr_info "$NAME" "$DOMAIN_ID")")
fi

# b) Alias == account name (self-conflict)
for a in "${ALIASES[@]:-}"; do
    [[ -z "$a" ]] && continue
    if [[ "$a" == "$NAME" ]]; then
        CONFLICTS+=("Alias '$a' is identical to the account name.")
    fi
done

# c) Aliases against existing addresses
for a in "${ALIASES[@]:-}"; do
    [[ -z "$a" ]] && continue
    if addr_taken "$a" "$DOMAIN_ID"; then
        CONFLICTS+=("Alias '$a' is already taken in this domain:")
        CONFLICTS+=("$(addr_info "$a" "$DOMAIN_ID")")
    fi
done

# --- Evaluation ---
if [[ ${#CONFLICTS[@]} -eq 0 ]]; then
    echo ""
    echo "No conflicts found."
    break
fi

echo ""
echo "!! Conflicts found:"
for c in "${CONFLICTS[@]}"; do
    echo "  $c"
done

echo ""
read -rp "[E] Re-enter data  /  [A] Abort: " CHOICE
case "${CHOICE,,}" in
    a)
        echo "Aborted. Nothing was created."
        exit 0
        ;;
    *)
        continue
        ;;
esac

done

---------------------------------------------------------------------------

4) Build aliases as an index-keyed map: {“0”:{…},“1”:{…}}

(Stalwart expects these collections as a map, NOT as an array.)

---------------------------------------------------------------------------

ALIASES_JSON=“{”
idx=0
first=true

for a in “${ALIASES[@]:-}”; do
[[ -z “$a” ]] && continue

if [[ "$first" == true ]]; then
    first=false
else
    ALIASES_JSON+=","
fi

ALIASES_JSON+="\"$idx\":{\"name\":\"$a\",\"domainId\":\"$DOMAIN_ID\",\"enabled\":true}"
idx=$((idx + 1))

done

ALIASES_JSON+=“}”

echo “Aliases JSON:”
echo “$ALIASES_JSON”

---------------------------------------------------------------------------

5) Create account

---------------------------------------------------------------------------

echo “== Creating account ==”

CREDENTIALS_JSON=$(jq -nc --arg password “$PASSWORD” ’
{
“0”: {
“@type”: “Password”,
“secret”: $password
}
}
')

CREATE_OUTPUT=$(
scli create Account/User 
–field “name=$NAME” 
–field “description=$FULL_NAME” 
–field “domainId=$DOMAIN_ID” 
–field “credentials=$CREDENTIALS_JSON” 
–field ‘memberGroupIds={}’ 
–field ‘roles={“@type”:“User”}’ 
–field ‘permissions={“@type”:“Inherit”}’ 
–field ‘locale=de_DE’ 
–field ‘timeZone=Europe/Berlin’ 
–field ‘quotas={“maxDiskQuota”:524288000}’ 
–field ‘aliases={}’ 
–field ‘encryptionAtRest={“@type”:“Disabled”}’
)

echo “$CREATE_OUTPUT”

Extract ID from “Created Account ” → guaranteed to be a single value.

ACCOUNT_ID=$(echo “$CREATE_OUTPUT” | awk ‘/Created Account/ {print $NF; exit}’)

if [[ -z “$ACCOUNT_ID” ]]; then
echo “ERROR: Could not read account ID from the create output.” >&2
exit 1
fi

echo “Account ID: $ACCOUNT_ID”

---------------------------------------------------------------------------

6) Set aliases (only if any were provided)

---------------------------------------------------------------------------

if [[ “$ALIASES_JSON” != “{}” ]]; then
echo “== Setting aliases ==”
scli update Account “$ACCOUNT_ID” --field “aliases=$ALIASES_JSON”
echo “Aliases set.”
else
echo “No aliases provided - skipping update.”
fi

echo “”
echo “Done: Account ‘$NAME’ created.”
echo “----------------------------------------”
echo “Account name: $NAME”
echo “Full name: $FULL_NAME”
echo “Domain ID: $DOMAIN_ID”
echo “Initial password: $PASSWORD”
echo “-----------------------