stalwart: choose private or public mail, and switch between them later

A mail server is two quite different products wearing one name, and until
now LibrePortal only offered the hard one. Installing Stalwart meant being
handed a wall of DNS records, a red error about port 25 and a warning about
reverse DNS — all of it correct, none of it fixable by the installer, and
most of it irrelevant to someone who wanted mailboxes and a shared calendar
on their own network.

CFG_STALWART_MODE now names which one you are running:

  private  mailboxes, IMAP, CalDAV and CardDAV on your own network. Port 25
           is not published at all; the client ports stay bound to the host
           but are never opened through the firewall. No MX, no PTR, no
           deliverability. Nothing to publish, so nothing is printed.
  public   the internet mail server, as before.
  auto     public if Traefik is installed, private if not, resolved at
           install and written back so it reads as a real answer afterwards.

DKIM keys are generated in both modes even though private has no use for
them today — that is what makes switching later a setting change rather
than a key ceremony. The WebUI gets a "Mail Exposure" tool that flips the
setting both ways and reconfigures the server, plus a "Show DNS Records"
tool that prints the live zone including current DKIM keys.

Two things this had to get right, both found by testing rather than
reading. Port access lives in the shell as CFG_<APP>_PORT_n, not just in
the config file, and the compose file is built from the parsed shell
values — editing only the file left the config claiming port 25 was
disabled while the container published it anyway. And going public needs
an AcmeProvider to exist before a domain can reference one, so the switch
creates it; note that doing so registers an account with Let's Encrypt.

Verified through real installs: auto resolves to private with no Traefik,
port 25 is genuinely unpublished and absent from the compose file, the
client ports are skipped by the firewall as host-bound, and the tool
round-trips private -> public -> private with the config landing back
exactly where it started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-18 22:52:55 +01:00
parent 9d6fd25c41
commit 88e9631b68
6 changed files with 437 additions and 31 deletions

View File

@ -36,6 +36,103 @@ stalwart_cli()
"ghcr.io/stalwartlabs/cli:${CFG_STALWART_CLI_VERSION:-1.0.12}" --no-color "$@"
}
# Resolve CFG_STALWART_MODE, turning 'auto' into a real answer.
#
# Traefik is the tell: LibrePortal only installs it once there is a domain
# pointed at this box, so its presence is a decent proxy for "this machine is
# meant to be reachable from the internet". Absent it, the safe reading is that
# nobody outside can reach this host anyway, and a public mail server would only
# produce mail that silently fails to deliver.
stalwart_mode()
{
local mode="${CFG_STALWART_MODE:-auto}"
if [[ "$mode" == "auto" ]]; then
if [[ -d "${containers_dir}traefik" ]]; then mode="public"; else mode="private"; fi
fi
[[ "$mode" == "public" || "$mode" == "private" ]] || mode="private"
printf '%s' "$mode"
}
# Rewrite the `access` field (4th, pipe-separated) of one port line, in BOTH
# places it has to change.
#
# sed rather than updateConfigOption because these values are themselves
# pipe-delimited, and the config-update path encodes `|` as %7C — round-tripping
# a port spec through it would mangle the very field we are editing.
#
# The second half is the part that is easy to miss: initializeAppVariables reads
# CFG_<APP>_PORT_n out of the SHELL, not off disk, and the compose file is built
# from what it parses. Editing only the file leaves the config claiming one thing
# while the running container does another.
stalwart_set_port_access()
{
local cfg="$1" key="$2" access="$3"
runFileOp sed -i -E "s#^(${key}=\"([^|\"]*\|){3})[^|\"]*#\1${access}#" "$cfg" 2>/dev/null
local cur="${!key}"
[[ -z "$cur" ]] && return 0
printf -v "$key" '%s' "$(printf '%s' "$cur" | sed -E "s#^(([^|]*\|){3})[^|]*#\1${access}#")"
}
# Put the mail ports where the chosen mode says they belong.
#
# Private does NOT simply firewall everything off: port 25 is dropped entirely
# (nothing should be delivering mail here from outside), while the client ports
# stay bound to the host under `private`, so a mail client on the LAN — or over
# the tailnet, if Headscale is in play — still reaches its mailbox. That
# distinction is the whole point: private means "not on the internet", not
# "switched off".
stalwart_apply_port_access()
{
local cfg="$1" mode="$2"
local smtp client
if [[ "$mode" == "private" ]]; then
smtp="disabled"; client="private"
else
smtp="public"; client="public"
fi
stalwart_set_port_access "$cfg" "CFG_STALWART_PORT_2" "$smtp" # 25 inbound mail
stalwart_set_port_access "$cfg" "CFG_STALWART_PORT_3" "$client" # 465 submissions
stalwart_set_port_access "$cfg" "CFG_STALWART_PORT_4" "$client" # 587 submission
stalwart_set_port_access "$cfg" "CFG_STALWART_PORT_5" "$client" # 993 imaps
}
# Runs after the deployed config exists but before the compose file is built
# from it — the only window where changing port exposure still reaches the
# running container.
stalwart_install_post_setup()
{
local app_name="$1"
local cfg="$containers_dir$app_name/$app_name.config"
[[ -f "$cfg" ]] || return 0
local mode; mode=$(stalwart_mode)
# Write the resolved answer back, so the WebUI shows what this install
# actually is instead of leaving the user to work out what 'auto' became.
if [[ "${CFG_STALWART_MODE:-auto}" == "auto" ]]; then
runFileOp sed -i -E "s#^CFG_STALWART_MODE=.*#CFG_STALWART_MODE=${mode}#" "$cfg" 2>/dev/null
isNotice "Mail exposure not set explicitly — using '${mode}' (Traefik $([[ "$mode" == public ]] && echo present || echo absent))."
fi
stalwart_apply_port_access "$cfg" "$mode"
# The port arrays were parsed from this config before this hook ran, and the
# compose file is filled in from those arrays, not from the file we just
# edited. Without re-reading, the access changes above are written to disk
# and then quietly ignored — the config says port 25 is disabled while the
# container publishes it anyway, which is the worst of both worlds.
if declare -F initializeAppVariables >/dev/null 2>&1; then
initializeAppVariables "$app_name" >/dev/null 2>&1
fi
if [[ "$mode" == "private" ]]; then
isNotice "Private mail server: port 25 stays closed, mail clients reach it on the local network only."
fi
}
# Wait for the admin HTTP listener. Used twice: once for the bootstrap listener
# before we configure anything, once for the real one after the restart.
stalwart_wait_http()
@ -95,10 +192,32 @@ stalwart_install_provision()
# generateDkimKeys makes the server own DKIM: it creates an Ed25519 and an
# RSA key, publishes both, and rotates them on a schedule. Hand-managed DKIM
# keys are, in practice, keys that never get rotated.
isNotice "Configuring Stalwart for ${mail_domain}"
# A private server has no route to a publicly-trusted certificate — there is
# no public name to validate — so asking for one only produces a failing
# renewal loop. DKIM keys are generated either way: they cost nothing, and
# having them already in place is what makes a later switch to public a
# setting change rather than a key ceremony.
local mode; mode=$(stalwart_mode)
local want_cert=true
[[ "$mode" == "private" ]] && want_cert=false
# Public + Traefik + no DNS provider is a real dead end worth naming. Stalwart
# validates over TLS-ALPN-01 (443) or HTTP-01 (80), and Traefik holds both, so
# the only route left is DNS-01 — which needs the DNS provider integration.
# Without it mail clients get a self-signed certificate on 993 and no amount
# of waiting fixes it.
if [[ "$mode" == "public" && -d "${containers_dir}traefik" \
&& ( "${CFG_STALWART_DNS_PROVIDER:-manual}" == "manual" || -z "${CFG_STALWART_DNS_API_TOKEN:-}" ) ]]; then
isNotice "Traefik owns ports 80 and 443, so Stalwart cannot validate a certificate"
isNotice " for its mail ports on its own. Set CFG_STALWART_DNS_PROVIDER and a token"
isNotice " to validate over DNS instead, or mail clients will see a self-signed"
isNotice " certificate on port 993."
fi
isNotice "Configuring Stalwart for ${mail_domain} (${mode})…"
local plan
plan=$(printf '{"@type":"update","object":"Bootstrap","value":{"serverHostname":"%s","defaultDomain":"%s","generateDkimKeys":true,"requestTlsCertificate":true}}' \
"$mail_host" "$mail_domain")
plan=$(printf '{"@type":"update","object":"Bootstrap","value":{"serverHostname":"%s","defaultDomain":"%s","generateDkimKeys":true,"requestTlsCertificate":%s}}' \
"$mail_host" "$mail_domain" "$want_cert")
# Keep the server's own error rather than swallowing it. The two that
# actually happen say exactly what is wrong — a hostname under a TLD that
@ -143,6 +262,10 @@ stalwart_install_dns_provider()
{
local provider="${CFG_STALWART_DNS_PROVIDER:-manual}"
[[ "$provider" == "manual" || -z "$provider" ]] && return 0
# A private server publishes nothing: there is no public zone to keep in
# sync, and handing a zone-write token to a box that never sends mail is
# blast radius bought for nothing.
[[ "$(stalwart_mode)" == "private" ]] && return 0
if [[ -z "${CFG_STALWART_DNS_API_TOKEN:-}" ]]; then
isError "CFG_STALWART_DNS_PROVIDER is set to '$provider' but no API token was given."
@ -200,7 +323,12 @@ stalwart_install_post_start()
((menu_number++))
echo ""
echo "---- $menu_number. Mail server checks + the DNS records you still need"
local mode; mode=$(stalwart_mode)
if [[ "$mode" == "private" ]]; then
echo "---- $menu_number. Setting up your private mail + calendar server"
else
echo "---- $menu_number. Mail server checks + the DNS records you still need"
fi
echo ""
# Resolved admin port comes from the compose tag (format `external:internal`),
@ -214,31 +342,38 @@ stalwart_install_post_start()
# ---- 1. Answer the setup wizard on the user's behalf ------------------
stalwart_install_provision
# ---- 2. Can this host even send mail? --------------------------------
# Most cheap VPS providers block outbound 25 by default (and several only
# unblock on request). A blocked port 25 means no mail EVER leaves the box,
# and nothing in the WebUI would otherwise reveal it.
isNotice "Checking outbound port 25 (required to deliver mail to other servers)…"
if command -v timeout >/dev/null 2>&1 \
&& timeout 8 bash -c 'exec 3<>/dev/tcp/gmail-smtp-in.l.google.com/25' 2>/dev/null; then
isSuccessful "Outbound port 25 is open."
else
isError "Outbound port 25 appears BLOCKED or filtered on this host."
isNotice " Most VPS providers block it by default. Ask your provider to unblock"
isNotice " outbound 25, or mail will queue and never deliver."
fi
# ---- 3. Reverse DNS --------------------------------------------------
# Receiving servers check that the sending IP resolves back to a name. A
# generic provider PTR (e.g. static.1.2.3.4.provider.net) is a common reason
# for mail being junked, and it can only be fixed in the provider's panel.
if [[ -n "$public_ip_v4" ]] && command -v dig >/dev/null 2>&1; then
local ptr; ptr=$(dig +short -x "$public_ip_v4" 2>/dev/null | head -1)
if [[ -n "$ptr" ]]; then
isNotice "Reverse DNS (PTR) for $public_ip_v4 is: ${ptr%.}"
isNotice " It should match your mail hostname. Set it in your VPS provider's panel."
# ---- 2 + 3. Internet-mail prerequisites ------------------------------
# Both of these are about exchanging mail with the outside world, so both
# are noise on a private server — worse than noise, since a red ERROR about
# port 25 on a server that is deliberately not on the internet reads as a
# broken install and sends people chasing a problem they do not have.
if [[ "$mode" == "public" ]]; then
# Most cheap VPS providers block outbound 25 by default (and several only
# unblock on request). A blocked port 25 means no mail EVER leaves the box,
# and nothing in the WebUI would otherwise reveal it.
isNotice "Checking outbound port 25 (required to deliver mail to other servers)…"
if command -v timeout >/dev/null 2>&1 \
&& timeout 8 bash -c 'exec 3<>/dev/tcp/gmail-smtp-in.l.google.com/25' 2>/dev/null; then
isSuccessful "Outbound port 25 is open."
else
isError "No reverse DNS (PTR) record for $public_ip_v4 — set one at your VPS provider."
isError "Outbound port 25 appears BLOCKED or filtered on this host."
isNotice " Most VPS providers block it by default. Ask your provider to unblock"
isNotice " outbound 25, or mail will queue and never deliver."
isNotice " If you did not mean to run an internet mail server, switch this app"
isNotice " to private in its Tools tab — none of this applies there."
fi
# Receiving servers check that the sending IP resolves back to a name. A
# generic provider PTR (e.g. static.1.2.3.4.provider.net) is a common reason
# for mail being junked, and it can only be fixed in the provider's panel.
if [[ -n "$public_ip_v4" ]] && command -v dig >/dev/null 2>&1; then
local ptr; ptr=$(dig +short -x "$public_ip_v4" 2>/dev/null | head -1)
if [[ -n "$ptr" ]]; then
isNotice "Reverse DNS (PTR) for $public_ip_v4 is: ${ptr%.}"
isNotice " It should match your mail hostname. Set it in your VPS provider's panel."
else
isError "No reverse DNS (PTR) record for $public_ip_v4 — set one at your VPS provider."
fi
fi
fi
@ -277,7 +412,25 @@ stalwart_install_post_start()
local mail_host="${host_setup:-your-mail-hostname}"
local mail_domain="${mail_host#*.}"
if [[ "${CFG_STALWART_DNS_PROVIDER:-manual}" != "manual" && -n "${CFG_STALWART_DNS_API_TOKEN:-}" ]]; then
if [[ "$mode" == "private" ]]; then
# Nothing to publish, so say what the user actually has instead — the
# honest headline is that everything below works today, with no DNS,
# no registrar and no waiting.
echo ""
isNotice "Nothing to add at a registrar — this server is not on the internet."
isNotice "What you have, working now, on your local network:"
echo " Mail between local accounts, IMAP on 993, submission on 465/587"
echo " Calendars (CalDAV) and contacts (CardDAV)"
echo ""
isNotice "Point mail and calendar clients at: ${mail_host}"
isNotice " Certificates are self-signed, so clients will ask you to trust it once."
[[ -d "${containers_dir}headscale" ]] && \
isNotice " Headscale is installed, so this also reaches you from anywhere on your tailnet."
echo ""
isNotice "To exchange mail with the internet later, switch this app to public"
isNotice " in its Tools tab. DKIM keys are already generated, so nothing is lost."
echo ""
elif [[ "${CFG_STALWART_DNS_PROVIDER:-manual}" != "manual" && -n "${CFG_STALWART_DNS_API_TOKEN:-}" ]]; then
isNotice "DNS is managed automatically — no records to add by hand."
isNotice " Still set at your provider, because they are not in the zone:"
echo " A ${mail_host} ${public_ip_v4:-<the IP of this server>}"
@ -319,6 +472,10 @@ stalwart_install_post_start()
isNotice "Sign in as '${CFG_STALWART_ADMIN_USER:-admin}' with the password shown below."
fi
isNotice "Until MX, PTR, SPF, DKIM and DMARC are all in place, expect delivery"
isNotice "problems — that is normal for a new mail server, not a fault in the app."
if [[ "$mode" == "public" ]]; then
isNotice "Until MX, PTR, SPF, DKIM and DMARC are all in place, expect delivery"
isNotice "problems — that is normal for a new mail server, not a fault in the app."
isNotice "Even once they are, large providers distrust a brand-new sending IP for"
isNotice "a while. That part is reputation, not configuration, and it takes time."
fi
}

View File

@ -60,6 +60,33 @@ CFG_STALWART_DOMAIN=1
CFG_STALWART_WHITELIST=false
CFG_STALWART_NETWORK=default
#
# MODE = who this mail server is for. This is the single most consequential
# setting on the app, so it is worth reading before changing.
#
# private — mail stays on your own network. Stalwart still gives you
# mailboxes, IMAP, calendars and contacts, and mail between local
# accounts works normally. It just does not exchange mail with the
# internet, so there is no MX, no reverse DNS, no port 25 and no
# deliverability to worry about. Port 25 is not published at all;
# the client ports stay bound to the host but are never opened
# through the firewall.
#
# public — a real internet mail server. Needs outbound AND inbound port 25,
# a matching reverse DNS record set by your VPS provider, and the
# DNS records the installer prints. Be aware that deliverability is
# a reputation game as much as a configuration one: a brand-new IP
# is distrusted by large providers for weeks regardless of how
# correct your setup is. That part no installer can fix for you.
#
# auto — decide at install time: public if Traefik is installed (which in
# practice means you have a domain pointed here), private if not.
# The resolved value is written back here, so this reads as a real
# answer afterwards rather than staying 'auto'.
#
# Changing this later is supported — see the "Mail exposure" tool in the WebUI,
# which flips the setting and reconfigures the server both ways.
CFG_STALWART_MODE=auto
#
# =============================================================================
# PORT CONFIGURATION
# =============================================================================

View File

@ -0,0 +1,32 @@
{
"tools": [
{
"id": "set_mode",
"category": "system",
"label": "Mail Exposure",
"description": "Switch between a private mail + calendar server and a real internet mail server.",
"icon": "📬",
"fields": [
{
"name": "mode",
"label": "Who is this mail server for?",
"type": "select",
"required": true,
"default": "private",
"options": [
{ "value": "private", "label": "Private — local network only, no internet mail" },
{ "value": "public", "label": "Public — exchange mail with the internet" }
]
}
]
},
{
"id": "show_dns",
"category": "system",
"label": "Show DNS Records",
"description": "Print the records this domain needs, including the live DKIM keys.",
"icon": "🌐",
"fields": []
}
]
}

View File

@ -0,0 +1,129 @@
#!/bin/bash
# Switch Stalwart between a private mail + calendar server and a real internet
# mail server, in either direction.
#
# The two ends of the switch are genuinely different jobs, and each half is
# applied by whichever layer owns it:
#
# the app config — which mail ports are exposed, and how. Read when the
# compose file is regenerated, so it needs a reinstall to
# reach the running container.
# Stalwart itself — whether it should hold a publicly-trusted certificate and
# publish DNS. Applied live over the management API.
#
# Going public is additive and safe to repeat. Going private deliberately does
# NOT destroy anything: DKIM keys stay, the domain stays, mailboxes stay. It
# closes the doors, it does not burn the house down — which is what makes
# flipping back a setting change rather than a rebuild.
appStalwartSetMode() {
local args="$1"
local mode
mode="$(authToolArg "$args" mode)"
if [[ "$mode" != "private" && "$mode" != "public" ]]; then
isError "Mode must be 'private' or 'public'."
return 1
fi
local cfg="${containers_dir}stalwart/stalwart.config"
if [[ ! -f "$cfg" ]]; then
isError "Stalwart does not look installed — no config at $cfg"
return 1
fi
local current
current=$(runFileOp grep -oE '^CFG_STALWART_MODE=\S*' "$cfg" 2>/dev/null | cut -d= -f2)
if [[ "$current" == "$mode" ]]; then
isNotice "Stalwart is already set to '$mode' — nothing to change."
return 0
fi
# ---- 1. The setting itself -------------------------------------------
runFileOp sed -i -E "s#^CFG_STALWART_MODE=.*#CFG_STALWART_MODE=${mode}#" "$cfg" 2>/dev/null
stalwart_apply_port_access "$cfg" "$mode"
isSuccessful "Mail exposure set to '$mode'."
# ---- 2. What Stalwart itself has to be told --------------------------
# Certificate management is the part that actually differs at runtime. A
# private server has no public name to validate, so asking for a certificate
# only produces a failing renewal loop; a public one needs the opposite.
# Ask the server which domain it serves rather than reconstructing it from
# install-time variables — $host_setup is populated during an install and is
# simply absent when a tool runs on its own.
local domain_id plan
domain_id=$(stalwart_cli query Domain 2>/dev/null | awk 'NR==2{print $1}')
if [[ -z "$domain_id" ]]; then
isNotice "Could not reach Stalwart to reconfigure it — the setting is saved and"
isNotice " will be applied when the app is reinstalled below."
else
if [[ "$mode" == "private" ]]; then
# Also drop automatic DNS: with no public role there is nothing to
# keep in sync, and leaving a zone-write token live is blast radius
# bought for nothing.
plan=$(printf '{"@type":"update","object":"Domain","id":"%s","value":{"certificateManagement":{"@type":"Manual"},"dnsManagement":{"@type":"Manual"}}}' \
"$domain_id")
else
# Automatic certificates need an ACME provider to point at, and a
# server that was private has never had one — so create it here
# rather than failing with "ACME provider not found".
#
# Two things about this are worth knowing. Creating the provider
# REGISTERS AN ACCOUNT with Let's Encrypt there and then, so it is a
# real outbound action, not a local setting. And the challenge type
# is not a free choice: TLS-ALPN-01 wants port 443 and HTTP-01 wants
# 80, both of which Traefik holds on a LibrePortal box, so DNS-01 is
# the only one that can succeed once DNS automation is available.
local challenge="TlsAlpn01"
[[ "${CFG_STALWART_DNS_PROVIDER:-manual}" != "manual" && -n "${CFG_STALWART_DNS_API_TOKEN:-}" ]] \
&& challenge="Dns01"
local domain_name contact
domain_name=$(stalwart_cli query Domain 2>/dev/null | awk 'NR==2{print $2}')
contact="postmaster@${domain_name}"
# `contact` is a set, so it goes as a map of value->true, not a list.
plan=$(printf '{"@type":"upsert","object":"AcmeProvider","matchOn":["directory"],"value":{"acme":{"directory":"https://acme-v02.api.letsencrypt.org/directory","contact":{"%s":true},"challengeType":"%s"}}}\n{"@type":"update","object":"Domain","id":"%s","value":{"certificateManagement":{"@type":"Automatic","acmeProviderId":"#acme"}}}' \
"$contact" "$challenge" "$domain_id")
if [[ "$challenge" == "TlsAlpn01" && -d "${containers_dir}traefik" ]]; then
isNotice "Traefik holds ports 80 and 443, so this certificate request will not"
isNotice " validate. Set CFG_STALWART_DNS_PROVIDER and a token to validate over"
isNotice " DNS instead — that is the only route that works alongside Traefik."
fi
fi
if printf '%s\n' "$plan" | stalwart_cli apply --stdin >/dev/null 2>&1; then
isSuccessful "Stalwart reconfigured for $mode operation."
else
isNotice "Stalwart is running but rejected the change — check it in the admin console."
fi
fi
# ---- 3. The firewall --------------------------------------------------
if declare -F firewallRebuildFromDatabase >/dev/null 2>&1; then
firewallRebuildFromDatabase >/dev/null 2>&1 \
&& isSuccessful "Firewall rules rebuilt."
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 [[ "$mode" == "public" ]]; then
echo ""
isNotice "Going public also needs, outside this box:"
isNotice " • outbound AND inbound port 25 (many providers block outbound by default)"
isNotice " • a reverse DNS (PTR) record matching ${host_setup:-your mail hostname}"
isNotice " • the DNS records — run the 'Show DNS Records' tool to list them"
fi
return 0
}

View File

@ -0,0 +1,43 @@
#!/bin/bash
# Print the DNS records this domain needs, read back from Stalwart rather than
# composed here.
#
# The server keeps the whole set in the domain's `dnsZoneFile`, which means this
# includes the live DKIM public keys — the one part nobody can write down in
# advance, and the part people most often come looking for after install.
# It also tracks reality: rotate a DKIM key and this shows the new one.
appStalwartShowDns() {
local domain_id zone
domain_id=$(stalwart_cli query Domain 2>/dev/null | awk 'NR==2{print $1}')
if [[ -z "$domain_id" ]]; then
isError "Could not read the domain from Stalwart — is it running and set up?"
return 1
fi
zone=$(stalwart_cli get Domain "$domain_id" 2>/dev/null \
| sed -n '/DNS Zone File:/,$p' | sed 's/^ *DNS Zone File: *//')
if [[ -z "$zone" ]]; then
isError "Stalwart returned no zone data for this domain."
return 1
fi
local mail_host="${host_setup:-your-mail-hostname}"
echo ""
if [[ "$(stalwart_mode)" == "private" ]]; then
isNotice "This server is private, so none of these need publishing today."
isNotice "They are what you would add if you switched it to public."
echo ""
fi
isNotice "DNS records for this mail server:"
echo " A ${mail_host} ${public_ip_v4:-<the IP of this server>}"
echo ""
printf '%s\n' "$zone" | sed 's/^/ /'
echo ""
isNotice "The A and PTR records are set at your host and provider; the rest go in the zone."
return 0
}

View File

@ -100,6 +100,8 @@ declare -gA LP_FN_MAP=(
[appSetupComposeTags_speedtest]="speedtest/scripts/speedtest_compose_tags.sh"
[appSetupComposeTags_vaultwarden]="vaultwarden/scripts/vaultwarden_compose_tags.sh"
[appSetupComposeTags_wireguard]="wireguard/scripts/wireguard_compose_tags.sh"
[appStalwartSetMode]="stalwart/tools/stalwart_set_mode.sh"
[appStalwartShowDns]="stalwart/tools/stalwart_show_dns.sh"
[appStatus]="app/app_status.sh"
[appStoatDisableUser]="stoat/tools/stoat_disable_user.sh"
[appStoatEnableUser]="stoat/tools/stoat_enable_user.sh"
@ -924,12 +926,16 @@ declare -gA LP_FN_MAP=(
[showInstructions]="menu/message/instructions.sh"
[sourceBackupLocations]="backup/locations/location_loader.sh"
[sshRemote]="network/ssh/ssh.sh"
[stalwart_apply_port_access]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_cli]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_dns_provider]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_first_mailbox]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_message_data]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_post_setup]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_post_start]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_install_provision]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_mode]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_set_port_access]="stalwart/scripts/stalwart_install_hooks.sh"
[stalwart_upgrade_admin_ui_code]="stalwart/scripts/stalwart_upgrade_hooks.sh"
[stalwart_upgrade_check_admin_ui]="stalwart/scripts/stalwart_upgrade_hooks.sh"
[stalwart_upgrade_verify]="stalwart/scripts/stalwart_upgrade_hooks.sh"
@ -1217,6 +1223,8 @@ declare -gA LP_FN_ROOT=(
[appSetupComposeTags_speedtest]="containers"
[appSetupComposeTags_vaultwarden]="containers"
[appSetupComposeTags_wireguard]="containers"
[appStalwartSetMode]="containers"
[appStalwartShowDns]="containers"
[appStatus]="scripts"
[appStoatDisableUser]="containers"
[appStoatEnableUser]="containers"
@ -2041,12 +2049,16 @@ declare -gA LP_FN_ROOT=(
[showInstructions]="scripts"
[sourceBackupLocations]="scripts"
[sshRemote]="scripts"
[stalwart_apply_port_access]="containers"
[stalwart_cli]="containers"
[stalwart_install_dns_provider]="containers"
[stalwart_install_first_mailbox]="containers"
[stalwart_install_message_data]="containers"
[stalwart_install_post_setup]="containers"
[stalwart_install_post_start]="containers"
[stalwart_install_provision]="containers"
[stalwart_mode]="containers"
[stalwart_set_port_access]="containers"
[stalwart_upgrade_admin_ui_code]="containers"
[stalwart_upgrade_check_admin_ui]="containers"
[stalwart_upgrade_verify]="containers"
@ -2369,6 +2381,8 @@ appSetupComposeTags_searxng() { unset -f appSetupComposeTags_searxng; __lpAutolo
appSetupComposeTags_speedtest() { unset -f appSetupComposeTags_speedtest; __lpAutoload "${install_containers_dir}speedtest/scripts/speedtest_compose_tags.sh"; appSetupComposeTags_speedtest "$@"; }
appSetupComposeTags_vaultwarden() { unset -f appSetupComposeTags_vaultwarden; __lpAutoload "${install_containers_dir}vaultwarden/scripts/vaultwarden_compose_tags.sh"; appSetupComposeTags_vaultwarden "$@"; }
appSetupComposeTags_wireguard() { unset -f appSetupComposeTags_wireguard; __lpAutoload "${install_containers_dir}wireguard/scripts/wireguard_compose_tags.sh"; appSetupComposeTags_wireguard "$@"; }
appStalwartSetMode() { unset -f appStalwartSetMode; __lpAutoload "${install_containers_dir}stalwart/tools/stalwart_set_mode.sh"; appStalwartSetMode "$@"; }
appStalwartShowDns() { unset -f appStalwartShowDns; __lpAutoload "${install_containers_dir}stalwart/tools/stalwart_show_dns.sh"; appStalwartShowDns "$@"; }
appStatus() { unset -f appStatus; __lpAutoload "${install_scripts_dir}app/app_status.sh"; appStatus "$@"; }
appStoatDisableUser() { unset -f appStoatDisableUser; __lpAutoload "${install_containers_dir}stoat/tools/stoat_disable_user.sh"; appStoatDisableUser "$@"; }
appStoatEnableUser() { unset -f appStoatEnableUser; __lpAutoload "${install_containers_dir}stoat/tools/stoat_enable_user.sh"; appStoatEnableUser "$@"; }
@ -3193,12 +3207,16 @@ setupWizardTerminal() { unset -f setupWizardTerminal; __lpAutoload "${install_sc
showInstructions() { unset -f showInstructions; __lpAutoload "${install_scripts_dir}menu/message/instructions.sh"; showInstructions "$@"; }
sourceBackupLocations() { unset -f sourceBackupLocations; __lpAutoload "${install_scripts_dir}backup/locations/location_loader.sh"; sourceBackupLocations "$@"; }
sshRemote() { unset -f sshRemote; __lpAutoload "${install_scripts_dir}network/ssh/ssh.sh"; sshRemote "$@"; }
stalwart_apply_port_access() { unset -f stalwart_apply_port_access; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_apply_port_access "$@"; }
stalwart_cli() { unset -f stalwart_cli; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_cli "$@"; }
stalwart_install_dns_provider() { unset -f stalwart_install_dns_provider; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_dns_provider "$@"; }
stalwart_install_first_mailbox() { unset -f stalwart_install_first_mailbox; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_first_mailbox "$@"; }
stalwart_install_message_data() { unset -f stalwart_install_message_data; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_message_data "$@"; }
stalwart_install_post_setup() { unset -f stalwart_install_post_setup; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_post_setup "$@"; }
stalwart_install_post_start() { unset -f stalwart_install_post_start; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_post_start "$@"; }
stalwart_install_provision() { unset -f stalwart_install_provision; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_install_provision "$@"; }
stalwart_mode() { unset -f stalwart_mode; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_mode "$@"; }
stalwart_set_port_access() { unset -f stalwart_set_port_access; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_install_hooks.sh"; stalwart_set_port_access "$@"; }
stalwart_upgrade_admin_ui_code() { unset -f stalwart_upgrade_admin_ui_code; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_upgrade_hooks.sh"; stalwart_upgrade_admin_ui_code "$@"; }
stalwart_upgrade_check_admin_ui() { unset -f stalwart_upgrade_check_admin_ui; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_upgrade_hooks.sh"; stalwart_upgrade_check_admin_ui "$@"; }
stalwart_upgrade_verify() { unset -f stalwart_upgrade_verify; __lpAutoload "${install_containers_dir}stalwart/scripts/stalwart_upgrade_hooks.sh"; stalwart_upgrade_verify "$@"; }