diff --git a/containers/stalwart/scripts/stalwart_install_hooks.sh b/containers/stalwart/scripts/stalwart_install_hooks.sh index 8214b98..84e8f04 100644 --- a/containers/stalwart/scripts/stalwart_install_hooks.sh +++ b/containers/stalwart/scripts/stalwart_install_hooks.sh @@ -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__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:-}" @@ -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 } diff --git a/containers/stalwart/stalwart.config b/containers/stalwart/stalwart.config index f1bc498..14478a6 100644 --- a/containers/stalwart/stalwart.config +++ b/containers/stalwart/stalwart.config @@ -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 # ============================================================================= diff --git a/containers/stalwart/tools/stalwart.tools.json b/containers/stalwart/tools/stalwart.tools.json new file mode 100644 index 0000000..8085b73 --- /dev/null +++ b/containers/stalwart/tools/stalwart.tools.json @@ -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": [] + } + ] +} diff --git a/containers/stalwart/tools/stalwart_set_mode.sh b/containers/stalwart/tools/stalwart_set_mode.sh new file mode 100644 index 0000000..2b59242 --- /dev/null +++ b/containers/stalwart/tools/stalwart_set_mode.sh @@ -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 +} diff --git a/containers/stalwart/tools/stalwart_show_dns.sh b/containers/stalwart/tools/stalwart_show_dns.sh new file mode 100644 index 0000000..de859aa --- /dev/null +++ b/containers/stalwart/tools/stalwart_show_dns.sh @@ -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:-}" + 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 +} diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 09e2bff..76dc347 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -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 "$@"; }