stalwart: make the mode switch finish the job itself

Switching between private and public wrote the setting, reconfigured the
server and then asked the user to run `libreportal app install stalwart`
to make the ports actually change. That left a window where the WebUI
reported public while port 25 was still closed — or worse, reported
private while 25 was still open and listening. A mode switch that does not
move the ports is not a mode switch.

The tool now runs the install itself. Safe from here: tools are dispatched
inline rather than as their own task, so this is not a nested task and
cannot deadlock on the task lock, and nothing in Stalwart's install hooks
calls back into the tool. Provisioning inside that install is a no-op
because it skips once config.json exists.

Dropped the separate firewall rebuild — the install reallocates the ports
and rebuilds the rules from the result, so doing it beforehand only worked
from the old allocation and was then immediately redone.

Verified both directions on a real install: private -> public publishes 25,
public -> private removes it, the admin port keeps its existing random
allocation across both (no --reset-network, so bookmarked WebUI links do
not move), mailboxes survive with their original creation timestamps, and
re-selecting the current mode is a no-op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-18 23:03:23 +01:00
parent 88e9631b68
commit af78ce1681

View File

@ -102,20 +102,41 @@ appStalwartSetMode() {
fi
fi
# ---- 3. The firewall --------------------------------------------------
if declare -F firewallRebuildFromDatabase >/dev/null 2>&1; then
firewallRebuildFromDatabase >/dev/null 2>&1 \
&& isSuccessful "Firewall rules rebuilt."
# ---- 3. Make the port change real --------------------------------------
# No separate firewall rebuild here: the install below reallocates ports and
# rebuilds the rules from the result. Rebuilding first would only work from
# the old allocation and then be immediately redone.
#
# Port publishing lives in the compose file, which is only written from the
# app config during an install — so without this the setting would be saved,
# the WebUI would show the new mode, and the container would carry on
# publishing exactly the ports it did before. A mode switch that leaves port
# 25 open is worse than no mode switch at all.
#
# Safe to call from here: tools are dispatched inline rather than as their
# own task, so this is not a nested task and cannot deadlock on the task
# lock. It also cannot recurse — the install path calls Stalwart's install
# hooks, and none of them call back into this tool. Provisioning inside that
# install is a no-op too, since it skips once config.json exists.
#
# No --reset-network: the admin port keeps its existing random allocation,
# so the WebUI link people have bookmarked does not move underneath them.
echo ""
isNotice "Applying the port changes (reinstalling the app — mail and mailboxes are kept)…"
if ! declare -F dockerInstallApp >/dev/null 2>&1; then
isError "Cannot reinstall automatically from here."
isNotice " Run it yourself to finish the switch: libreportal app install stalwart"
return 1
fi
# ---- 4. What is left ---------------------------------------------------
# Port publishing lives in the compose file, which is written from the app
# config at install time. Saying this plainly beats letting someone believe
# port 25 closed when it is still bound.
echo ""
isNotice "One step left: reinstall the app so the port changes reach the container."
isNotice " libreportal app install stalwart"
isNotice " Your mail, mailboxes and DKIM keys are untouched by this."
if dockerInstallApp "stalwart" "" "false"; then
isSuccessful "Stalwart is now running in '$mode' mode."
else
isError "The reinstall did not complete — the setting is saved, but the ports"
isNotice " have not changed yet. Retry with: libreportal app install stalwart"
return 1
fi
if [[ "$mode" == "public" ]]; then
echo ""