Files
Stefan HildebrandtandLinus Rath 578339c400 test(integration): composer From offers shared/group identities (#569)
Provision a Stalwart group (team@example.org) with carol as a member before her
first login, and assert the composer's From selector offers the group address.
This confirms the group-membership scenario of #569 already works out of the
box: Stalwart returns the group's send-as identity on the member's own account,
so the app's normal single-account identity load surfaces it (identities.length
> 1 -> the From <select> renders with team@).

- stalwart: create the `team` Group in plan-accounts and add carol via
  User.memberGroupIds in the entrypoint (id resolved after apply, like
  DOMAIN_ID). carol, not alice/bob, so the sync specs stay unshared.
- helpers: GROUP config, openComposer/composerFromOptions, and JmapClient
  accounts + sharedAccountNames (Identity/get needs the submission capability).
- composer: add data-testid="composer-from" to the From <select> and its
  single-identity <span> fallback.

Ref: https://github.com/bulwarkmail/webmail/issues/569
2026-07-16 17:59:52 +02:00

155 lines
5.9 KiB
Bash
Executable File

#!/bin/sh
# Declarative bootstrap for the integration-test Stalwart Mail Server.
#
# Phase 1 (first start only, marker absent):
# - Stalwart starts in bootstrap mode (no config.json -> HTTP on :8080).
# - plan-bootstrap.ndjson is applied via stalwart-cli. This writes
# config.json, initialises RocksDB and creates the default domain +
# admin account.
# - Stalwart is restarted in normal mode (config.json now exists).
# - plan-accounts.ndjson.tpl is materialised with the resolved DOMAIN_ID
# and the shared TEST_ACCOUNT_PASSWORD, then applied (test accounts +
# submission/IMAP listeners + cleartext auth for the dev lanes).
# - Stalwart is stopped and the marker is written.
#
# Phase 2 (regular start, marker present):
# - exec stalwart as PID 1.
#
# Adapted from examples/docker/stalwart for webmail<->Stalwart integration
# testing: no ticket/service accounts, no Sieve, a single shared password for
# the alice/bob/carol test mailboxes.
set -eu
# stalwart-cli caches its schema under $HOME/.cache/stalwart-cli. The stalwart
# user has no home, so redirect to /tmp.
export HOME=/tmp
DATA_DIR=/var/lib/stalwart
MARKER="${DATA_DIR}/.bootstrap-applied"
PLAN_DIR=/etc/stalwart-bootstrap
STALWART_BIN=/usr/local/bin/stalwart
STALWART_CLI=/usr/local/bin/stalwart-cli
STALWART_CFG=/etc/stalwart/config.json
LOCAL_URL=http://127.0.0.1:8080
log() { printf '[stalwart-bootstrap] %s\n' "$*" >&2; }
wait_for_http() {
for _ in $(seq 1 60); do
if curl -fsS -u "admin:${ADMIN_PASS}" "${LOCAL_URL}/jmap/session" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
log "Stalwart HTTP on :8080 did not come up in time"
return 1
}
run_stalwart_bg() {
"${STALWART_BIN}" --config "${STALWART_CFG}" &
STALWART_PID=$!
}
stop_stalwart_bg() {
if [ -n "${STALWART_PID:-}" ]; then
kill -TERM "${STALWART_PID}" 2>/dev/null || true
wait "${STALWART_PID}" 2>/dev/null || true
STALWART_PID=
fi
}
if [ ! -f "${MARKER}" ]; then
: "${STALWART_RECOVERY_ADMIN:?must be set for first-run bootstrap}"
: "${TEST_ACCOUNT_PASSWORD:?must be set for first-run bootstrap}"
ADMIN_PASS=${STALWART_RECOVERY_ADMIN#*:}
log "Phase 1: starting Stalwart in bootstrap mode"
run_stalwart_bg
wait_for_http
log "Applying plan-bootstrap.ndjson"
STALWART_URL=${LOCAL_URL} \
STALWART_USER=admin \
STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" apply --file "${PLAN_DIR}/plan-bootstrap.ndjson" --quiet
log "Restarting Stalwart to leave bootstrap mode"
stop_stalwart_bg
run_stalwart_bg
wait_for_http
log "Resolving DOMAIN_ID for example.org"
DOMAIN_ID=$(STALWART_URL=${LOCAL_URL} STALWART_USER=admin STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" query Domain --json 2>/dev/null \
| head -1 \
| sed -E 's/.*"id":"([^"]+)".*/\1/')
if [ -z "${DOMAIN_ID}" ]; then
log "Could not resolve DOMAIN_ID after bootstrap"
stop_stalwart_bg
exit 1
fi
log "DOMAIN_ID=${DOMAIN_ID}"
# Materialise the account plan. gettext/envsubst is not in the base image,
# so substitute the two placeholders with sed. Passwords are escaped for the
# sed replacement (& and / are the only metacharacters that matter here).
PLAN_ACCOUNTS=/tmp/plan-accounts.ndjson
esc_pw=$(printf '%s' "${TEST_ACCOUNT_PASSWORD}" | sed -e 's/[&/\\]/\\&/g')
sed -e "s/\${DOMAIN_ID}/${DOMAIN_ID}/g" \
-e "s/\${TEST_ACCOUNT_PASSWORD}/${esc_pw}/g" \
"${PLAN_DIR}/plan-accounts.ndjson.tpl" > "${PLAN_ACCOUNTS}"
log "Applying plan-accounts.ndjson"
STALWART_URL=${LOCAL_URL} \
STALWART_USER=admin \
STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" apply --file "${PLAN_ACCOUNTS}" --quiet
rm -f "${PLAN_ACCOUNTS}"
# Make 'carol' a member of the 'team' group *before her first login*, so she
# already has the shared group mailbox (its folders show under "Shared") and
# the team@ send-as identity. This provisions the issue #569 scenario: the
# composer's From dropdown should then offer the group address. Membership is
# a set keyed by the group's server-assigned account id (see the User schema's
# memberGroupIds), so the ids are resolved here, mirroring DOMAIN_ID above.
# carol (not alice/bob) is used so the single-/multi-account sync specs, which
# drive alice and bob, keep a clean unshared environment.
q_account_id() {
STALWART_URL=${LOCAL_URL} STALWART_USER=admin STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" query Account --json 2>/dev/null \
| grep "\"emailAddress\":\"$1\"" \
| sed -E 's/.*"id":"([^"]+)".*/\1/'
}
CAROL_ID=$(q_account_id "carol@example.org")
TEAM_ID=$(q_account_id "team@example.org")
if [ -n "${CAROL_ID}" ] && [ -n "${TEAM_ID}" ]; then
log "Adding carol (${CAROL_ID}) to the team group (${TEAM_ID}) [issue #569]"
STALWART_URL=${LOCAL_URL} STALWART_USER=admin STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" update Account "${CAROL_ID}" \
--field "memberGroupIds={\"${TEAM_ID}\":true}" >/dev/null
else
log "WARNING: could not resolve account ids (carol='${CAROL_ID}' team='${TEAM_ID}'); skipping group membership"
fi
# Default inbound throttles (sender->recipient + sender-IP) otherwise trip
# 452 4.4.5 when a test blasts many messages. Stalwart re-seeds the defaults
# on every start when absent, so deleting is useless; disable them instead,
# which survives restarts.
for tid in $(STALWART_URL=${LOCAL_URL} STALWART_USER=admin STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" query MtaInboundThrottle --json 2>/dev/null \
| sed -E 's/.*"id":"([^"]+)".*/\1/'); do
log "Disabling MtaInboundThrottle ${tid}"
STALWART_URL=${LOCAL_URL} STALWART_USER=admin STALWART_PASSWORD=${ADMIN_PASS} \
"${STALWART_CLI}" update MtaInboundThrottle "${tid}" --field enable=false >/dev/null
done
log "Stopping bootstrap instance, marking complete"
stop_stalwart_bg
touch "${MARKER}"
fi
log "Starting Stalwart (final, foreground)"
exec "${STALWART_BIN}" --config "${STALWART_CFG}"