diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index b4e125ab..3213acbb 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -14,17 +14,12 @@ use crate::{ manager::defaults::BootstrapDefaults, }; use arc_swap::ArcSwap; -use pwhash::sha512_crypt; use std::{ net::{IpAddr, Ipv4Addr}, path::PathBuf, sync::Arc, }; -use store::{ - RegistryStore, - rand::{Rng, distr::Alphanumeric, rng}, - registry::bootstrap::Bootstrap, -}; +use store::{RegistryStore, registry::bootstrap::Bootstrap}; use tokio::sync::{Notify, mpsc}; use utils::{UnwrapFailure, failed}; @@ -54,7 +49,6 @@ Options: -e, --export Export all store data to a specific path -i, --import Import store data from a specific path -o, --console Open the store console - -I, --init Initialize a new server at a specific path -h, --help Print help -V, --version Print version "# @@ -99,10 +93,6 @@ impl BootManager { ("config" | "c", Some(value)) => { config_path = Some(value); } - ("init" | "I", Some(value)) => { - quickstart(value); - std::process::exit(0); - } ("export" | "e", Some(value)) => { import_export = StoreOp::Export(BackupParams::new(value.into())); } @@ -308,189 +298,3 @@ pub fn build_ipc(has_pubsub: bool) -> (Ipc, IpcReceivers) { }, ) } - -fn quickstart(path: impl Into) { - let path = path.into(); - - if !path.exists() { - std::fs::create_dir_all(&path).failed("Failed to create directory"); - } - - for dir in &["etc", "data", "logs"] { - let sub_path = path.join(dir); - if !sub_path.exists() { - std::fs::create_dir(sub_path).failed(&format!("Failed to create {dir} directory")); - } - } - - let admin_pass = std::env::var("STALWART_ADMIN_PASSWORD").unwrap_or_else(|_| { - rng() - .sample_iter(Alphanumeric) - .take(10) - .map(char::from) - .collect::() - }); - - std::fs::write( - path.join("etc").join("registry.json"), - QUICKSTART_CONFIG - .replace("_P_", &path.to_string_lossy()) - .replace("_S_", &sha512_crypt::hash(&admin_pass).unwrap()), - ) - .failed("Failed to write configuration file"); - - eprintln!( - "✅ Local registry initialized at {}/etc/registry.json", - path.to_string_lossy() - ); - eprintln!("🔑 Your administrator account is 'admin' with password '{admin_pass}'."); -} - -#[cfg(not(feature = "foundation"))] -const QUICKSTART_CONFIG: &str = r#"[server.listener.smtp] -bind = "[::]:25" -protocol = "smtp" - -[server.listener.submission] -bind = "[::]:587" -protocol = "smtp" - -[server.listener.submissions] -bind = "[::]:465" -protocol = "smtp" -tls.implicit = true - -[server.listener.imap] -bind = "[::]:143" -protocol = "imap" - -[server.listener.imaptls] -bind = "[::]:993" -protocol = "imap" -tls.implicit = true - -[server.listener.pop3] -bind = "[::]:110" -protocol = "pop3" - -[server.listener.pop3s] -bind = "[::]:995" -protocol = "pop3" -tls.implicit = true - -[server.listener.sieve] -bind = "[::]:4190" -protocol = "managesieve" - -[server.listener.https] -protocol = "http" -bind = "[::]:443" -tls.implicit = true - -[server.listener.http] -protocol = "http" -bind = "[::]:8080" - -[storage] -data = "rocksdb" -fts = "rocksdb" -blob = "rocksdb" -lookup = "rocksdb" -directory = "internal" - -[store.rocksdb] -type = "rocksdb" -path = "_P_/data" -compression = "lz4" - -[directory.internal] -type = "internal" -store = "rocksdb" - -[tracer.log] -type = "log" -level = "info" -path = "_P_/logs" -prefix = "stalwart.log" -rotate = "daily" -ansi = false -enable = true - -[authentication.fallback-admin] -user = "admin" -secret = "_S_" -"#; - -#[cfg(feature = "foundation")] -const QUICKSTART_CONFIG: &str = r#"[server.listener.smtp] -bind = "[::]:25" -protocol = "smtp" - -[server.listener.submission] -bind = "[::]:587" -protocol = "smtp" - -[server.listener.submissions] -bind = "[::]:465" -protocol = "smtp" -tls.implicit = true - -[server.listener.imap] -bind = "[::]:143" -protocol = "imap" - -[server.listener.imaptls] -bind = "[::]:993" -protocol = "imap" -tls.implicit = true - -[server.listener.pop3] -bind = "[::]:110" -protocol = "pop3" - -[server.listener.pop3s] -bind = "[::]:995" -protocol = "pop3" -tls.implicit = true - -[server.listener.sieve] -bind = "[::]:4190" -protocol = "managesieve" - -[server.listener.https] -protocol = "http" -bind = "[::]:443" -tls.implicit = true - -[server.listener.http] -protocol = "http" -bind = "[::]:8080" - -[storage] -data = "foundation-db" -fts = "foundation-db" -blob = "foundation-db" -lookup = "foundation-db" -directory = "internal" - -[store.foundation-db] -type = "foundationdb" -compression = "lz4" - -[directory.internal] -type = "internal" -store = "foundation-db" - -[tracer.log] -type = "log" -level = "info" -path = "_P_/logs" -prefix = "stalwart.log" -rotate = "daily" -ansi = false -enable = true - -[authentication.fallback-admin] -user = "admin" -secret = "_S_" -"#; diff --git a/crates/store/src/build/registry.rs b/crates/store/src/build/registry.rs index f725399c..4157441c 100644 --- a/crates/store/src/build/registry.rs +++ b/crates/store/src/build/registry.rs @@ -15,6 +15,7 @@ use crate::{ now, }, }; +use rand::{Rng, distr::Alphanumeric, rng}; use registry::{ schema::{enums::ClusterNodeStatus, structs::ClusterNode}, types::datetime::UTCDateTime, @@ -36,6 +37,33 @@ impl RegistryStore { RegistryInit::Err(err) => return Err(err), RegistryInit::Bootstrap => { inner.env_recovery_mode = true; + + if inner.env_recovery_admin.is_none() { + let password = rng() + .sample_iter(Alphanumeric) + .take(16) + .map(char::from) + .collect::(); + eprintln!(); + eprintln!("════════════════════════════════════════════════════════════"); + eprintln!("🔑 Stalwart bootstrap mode — temporary administrator account"); + eprintln!(); + eprintln!(" username: admin"); + eprintln!(" password: {password}"); + eprintln!(); + eprintln!("Use these credentials to complete the initial setup at the"); + eprintln!("/admin web UI. Once setup is done, Stalwart will provision a"); + eprintln!("permanent administrator and this temporary account will no"); + eprintln!("longer apply."); + eprintln!(); + eprintln!("This password is shown only once. To pin a credential"); + eprintln!("instead, set STALWART_RECOVERY_ADMIN=admin: in the"); + eprintln!("env file."); + eprintln!("════════════════════════════════════════════════════════════"); + eprintln!(); + inner.env_recovery_admin = Some(("admin".to_string(), password)); + } + EphemeralStore::open() } }; diff --git a/crates/store/src/registry/local.rs b/crates/store/src/registry/local.rs index 2146c793..e3394f1f 100644 --- a/crates/store/src/registry/local.rs +++ b/crates/store/src/registry/local.rs @@ -26,15 +26,13 @@ impl RegistryStoreInner { .ok() .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .unwrap_or(false), - env_recovery_admin: std::env::var("STALWART_ADMIN_ACCOUNT") + env_recovery_admin: std::env::var("STALWART_RECOVERY_ADMIN") .ok() - .filter(|a| !a.is_empty()) - .and_then(|a| { - std::env::var("STALWART_ADMIN_PASS") - .ok() - .filter(|p| !p.is_empty()) - .map(|p| (a, p)) - }), + .and_then(|v| { + v.split_once(':') + .map(|(a, p)| (a.trim().to_string(), p.trim().to_string())) + }) + .filter(|(a, p)| !a.is_empty() && !p.is_empty()), env_cluster_role: std::env::var("STALWART_ROLE") .ok() .filter(|r| !r.is_empty()), diff --git a/install.sh b/install.sh index 4f6c8356..841e7275 100644 --- a/install.sh +++ b/install.sh @@ -19,136 +19,256 @@ main() { need_cmd uname need_cmd mktemp need_cmd chmod + need_cmd chown need_cmd mkdir need_cmd rm - need_cmd rmdir need_cmd tar + need_cmd cp + need_cmd hostname - # Make sure we are running as root - if [ "$(id -u)" -ne 0 ] ; then + # Require root + if [ "$(id -u)" -ne 0 ]; then err "❌ Install failed: This program needs to run as root." fi # Detect OS - local _os="unknown" - local _uname="$(uname)" + local _os _uname _account + _uname="$(uname)" _account="stalwart" - if [ "${_uname}" = "Linux" ]; then - _os="linux" - elif [ "${_uname}" = "Darwin" ]; then - _os="macos" - _account="_stalwart" - fi + case "$_uname" in + Linux) _os="linux" ;; + Darwin) _os="macos"; _account="_stalwart" ;; + *) err "❌ Install failed: Unsupported OS: $_uname" ;; + esac - # Read arguments - local _dir="/opt/stalwart" - - # Default component setting + # Parse arguments local _component="stalwart" - - # Loop through the arguments - for arg in "$@"; do - case "$arg" in + local _prefix="" + while [ $# -gt 0 ]; do + case "$1" in --fdb) _component="stalwart-foundationdb" ;; + -h|--help) + print_usage + exit 0 + ;; + --*|-*) + err "❌ Unknown flag: $1 (try --help)" + ;; *) - if [ -n "$arg" ]; then - _dir=$arg + if [ -n "$_prefix" ]; then + err "❌ Only one prefix argument is allowed, got: $_prefix $1" fi + _prefix="$1" ;; esac + shift done - # Detect platform architecture + # Derive install paths — FHS by default, self-contained under a custom prefix + local _bin_dir _bin_file _conf_dir _log_dir _data_dir _env_file _config_file + if [ -z "$_prefix" ]; then + _bin_dir="/usr/local/bin" + _conf_dir="/etc/stalwart" + _log_dir="/var/log/stalwart" + _data_dir="/var/lib/stalwart" + else + _bin_dir="${_prefix}/bin" + _conf_dir="${_prefix}/etc" + _log_dir="${_prefix}/logs" + _data_dir="${_prefix}/data" + fi + _bin_file="${_bin_dir}/stalwart" + _config_file="${_conf_dir}/config.json" + _env_file="${_conf_dir}/stalwart.env" + + # Detect architecture get_architecture || return 1 local _arch="$RETVAL" assert_nz "$_arch" "arch" + # Create service account + create_account "$_os" "$_account" + # Create directories - ensure mkdir -p "$_dir" "$_dir/bin" "$_dir/etc" "$_dir/logs" + ensure mkdir -p "$_bin_dir" "$_conf_dir" "$_log_dir" "$_data_dir" - # Download latest binary + # Download and install the binary say "⏳ Downloading ${_component} for ${_arch}..." - local _file="${_dir}/bin/stalwart.tar.gz" - local _url="${BASE_URL}/${_component}-${_arch}.tar.gz" - ensure mkdir -p "$_dir" - ensure downloader "$_url" "$_file" "$_arch" - ensure tar zxvf "$_file" -C "$_dir/bin" + local _tmp _tar _src_name + _tmp="$(mktemp -d)" + _tar="${_tmp}/stalwart.tar.gz" + ensure downloader "${BASE_URL}/${_component}-${_arch}.tar.gz" "$_tar" "$_arch" + ensure tar zxf "$_tar" -C "$_tmp" + _src_name="stalwart" if [ "$_component" = "stalwart-foundationdb" ]; then - ignore mv "$_dir/bin/stalwart-foundationdb" "$_dir/bin/stalwart" + _src_name="stalwart-foundationdb" fi - ignore chmod +x "$_dir/bin/stalwart" - ignore rm "$_file" + ensure cp "${_tmp}/${_src_name}" "$_bin_file" + ensure chmod 0755 "$_bin_file" + ensure rm -rf "$_tmp" - # Create system account - if ! id -u ${_account} > /dev/null 2>&1; then - say "🖥️ Creating '${_account}' account..." - if [ "${_os}" = "macos" ]; then - local _last_uid="$(dscacheutil -q user | grep uid | awk '{print $2}' | sort -n | tail -n 1)" - local _last_gid="$(dscacheutil -q group | grep gid | awk '{print $2}' | sort -n | tail -n 1)" - local _uid="$((_last_uid+1))" - local _gid="$((_last_gid+1))" - - ensure dscl /Local/Default -create Groups/_stalwart - ensure dscl /Local/Default -create Groups/_stalwart Password \* - ensure dscl /Local/Default -create Groups/_stalwart PrimaryGroupID $_gid - ensure dscl /Local/Default -create Groups/_stalwart RealName "Stalwart service" - ensure dscl /Local/Default -create Groups/_stalwart RecordName _stalwart stalwart - - ensure dscl /Local/Default -create Users/_stalwart - ensure dscl /Local/Default -create Users/_stalwart NFSHomeDirectory /Users/_stalwart - ensure dscl /Local/Default -create Users/_stalwart Password \* - ensure dscl /Local/Default -create Users/_stalwart PrimaryGroupID $_gid - ensure dscl /Local/Default -create Users/_stalwart RealName "Stalwart service" - ensure dscl /Local/Default -create Users/_stalwart RecordName _stalwart stalwart - ensure dscl /Local/Default -create Users/_stalwart UniqueID $_uid - ensure dscl /Local/Default -create Users/_stalwart UserShell /bin/bash - - ensure dscl /Local/Default -delete /Users/_stalwart AuthenticationAuthority - ensure dscl /Local/Default -delete /Users/_stalwart PasswordPolicyOptions - else - ensure useradd ${_account} -s /usr/sbin/nologin -M -r -U - fi + # Create env file if absent (preserve user edits on reinstall) + if [ ! -e "$_env_file" ]; then + say "📝 Writing env file at ${_env_file}..." + write_env_file "$_env_file" fi - # Run init - ignore $_dir/bin/stalwart --init "$_dir" - - # Set permissions + # Ownership and permissions say "🔐 Setting permissions..." - ensure chown -R ${_account}:${_account} "$_dir" - ensure chmod -R 755 "$_dir" - ensure chmod 700 "$_dir/etc/config.toml" + ensure chown "${_account}:${_account}" "$_conf_dir" "$_log_dir" "$_data_dir" + ensure chmod 0750 "$_conf_dir" "$_log_dir" "$_data_dir" + ensure chown "root:${_account}" "$_env_file" + ensure chmod 0640 "$_env_file" - # Create service file + # Install and start the service say "🚀 Starting service..." - if [ "${_os}" = "linux" ]; then - local _issystemdlinux=$(command -v systemctl) - if [ -n "$_issystemdlinux" ]; then - create_service_linux_systemd "$_dir" - else - create_service_linux_initd "$_dir" - fi - elif [ "${_os}" = "macos" ]; then - create_service_macos "$_dir" - fi + local _service_type="" + case "$_os" in + linux) + if check_cmd systemctl; then + create_service_linux_systemd "$_bin_file" "$_config_file" "$_env_file" "$_account" + _service_type="systemd" + else + create_service_linux_initd "$_bin_file" "$_config_file" "$_env_file" "$_account" + _service_type="initd" + fi + ;; + macos) + create_service_macos "$_bin_file" "$_config_file" "$_env_file" "$_account" + _service_type="launchd" + ;; + esac - # Installation complete - local _host=$(hostname -f) - say "🎉 Installation complete! Continue the setup at http://$_host:8080/login" + # Completion message + local _host + _host="$(hostname -f 2>/dev/null || hostname)" + say "" + say "🎉 Installation complete!" + say "" + say "Stalwart is running in bootstrap mode. A temporary administrator" + say "password was generated at startup and printed to the service logs." + say "" + say "👉 To find the password, inspect the service logs:" + case "$_service_type" in + systemd) + say " journalctl -u stalwart -n 200 | grep -A8 'bootstrap mode'" + ;; + initd) + say " grep -A8 'bootstrap mode' /var/log/syslog 2>/dev/null \\" + say " || grep -A8 'bootstrap mode' /var/log/messages" + ;; + launchd) + say " sudo log show --predicate 'process == \"stalwart\"' --last 5m" + ;; + esac + say "" + say " Or set STALWART_RECOVERY_ADMIN=admin: in" + say " ${_env_file} and restart the service to pin a credential." + say "" + say " Finish setup at: http://${_host}:8080/admin" + say "" return 0 } -# Functions to create service files +print_usage() { + cat <<'EOF' +Usage: install.sh [--fdb] [PREFIX] + +Install Stalwart into standard FHS paths or under a custom prefix. + +Options: + --fdb Install the FoundationDB build. + -h, --help Show this help. + +With no PREFIX, Stalwart is installed under standard FHS paths: + binary /usr/local/bin/stalwart + config /etc/stalwart/config.json (created by the daemon on first run) + env /etc/stalwart/stalwart.env + logs /var/log/stalwart/ + data /var/lib/stalwart/ + +When PREFIX is provided, a self-contained layout is used instead: + binary $PREFIX/bin/stalwart + config $PREFIX/etc/config.json + env $PREFIX/etc/stalwart.env + logs $PREFIX/logs/ + data $PREFIX/data/ +EOF +} + +write_env_file() { + cat > "$1" <<'EOF' +# Environment variables for the Stalwart service. +# Uncomment and edit an entry to override its default. + +# Enable bootstrap / recovery mode on startup. Accepted: 1, true. Default: false. +#STALWART_RECOVERY_MODE=true + +# Log level while in recovery mode. Default: info. +#STALWART_RECOVERY_MODE_LOG_LEVEL=debug + +# HTTP port used in recovery mode. Default: 8080. +#STALWART_RECOVERY_MODE_PORT=9090 + +# Fixed administrator credentials — format: username:password +# Default: a temporary random password is generated and printed to the logs. +#STALWART_RECOVERY_ADMIN=admin:changeme + +# Cluster role assigned to this node. Must match a role name defined in the +# cluster registry. Leave unset for a standalone (non-clustered) deployment. +#STALWART_ROLE=primary + +# Push-notification shard this node is responsible for, when running in a +# cluster. +#STALWART_PUSH_SHARD=1 +EOF +} + +create_account() { + local _os="$1" + local _account="$2" + if id -u "$_account" > /dev/null 2>&1; then + return 0 + fi + say "🖥️ Creating '${_account}' account..." + if [ "$_os" = "macos" ]; then + local _last_uid _last_gid _uid _gid + _last_uid="$(dscacheutil -q user | grep uid | awk '{print $2}' | sort -n | tail -n 1)" + _last_gid="$(dscacheutil -q group | grep gid | awk '{print $2}' | sort -n | tail -n 1)" + _uid="$((_last_uid+1))" + _gid="$((_last_gid+1))" + + ensure dscl /Local/Default -create Groups/_stalwart + ensure dscl /Local/Default -create Groups/_stalwart Password \* + ensure dscl /Local/Default -create Groups/_stalwart PrimaryGroupID $_gid + ensure dscl /Local/Default -create Groups/_stalwart RealName "Stalwart service" + ensure dscl /Local/Default -create Groups/_stalwart RecordName _stalwart stalwart + + ensure dscl /Local/Default -create Users/_stalwart + ensure dscl /Local/Default -create Users/_stalwart NFSHomeDirectory /var/empty + ensure dscl /Local/Default -create Users/_stalwart Password \* + ensure dscl /Local/Default -create Users/_stalwart PrimaryGroupID $_gid + ensure dscl /Local/Default -create Users/_stalwart RealName "Stalwart service" + ensure dscl /Local/Default -create Users/_stalwart RecordName _stalwart stalwart + ensure dscl /Local/Default -create Users/_stalwart UniqueID $_uid + ensure dscl /Local/Default -create Users/_stalwart UserShell /usr/bin/false + + ensure dscl /Local/Default -delete /Users/_stalwart AuthenticationAuthority + ensure dscl /Local/Default -delete /Users/_stalwart PasswordPolicyOptions + else + ensure useradd "$_account" -s /usr/sbin/nologin -M -r -U + fi +} + create_service_linux_systemd() { - local _dir="$1" - cat < /etc/systemd/system/stalwart.service + local _bin="$1" _config="$2" _env="$3" _user="$4" + cat > /etc/systemd/system/stalwart.service < /etc/init.d/stalwart + local _bin="$1" _config="$2" _env="$3" _user="$4" + cat > /etc/init.d/stalwart < /dev/null \ + start-stop-daemon --start --quiet --pidfile \$PIDFILE --exec \$DAEMON --test > /dev/null \\ || return 1 - start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ - --background --make-pidfile --chuid stalwart:stalwart \ - -- $DAEMON_ARGS \ + start-stop-daemon --start --quiet --pidfile \$PIDFILE --exec \$DAEMON \\ + --background --make-pidfile --chuid ${_user}:${_user} \\ + -- \$DAEMON_ARGS \\ || return 2 } do_stop() { - # Return - # 0 if daemon has been stopped - # 1 if daemon was already stopped - # 2 if daemon could not be stopped - # other if a failure occurred - start-stop-daemon --stop --quiet --retry=INT/30/KILL/5 --pidfile $PIDFILE --name stalwart - RETVAL="$?" - [ "$RETVAL" = 2 ] && return 2 - # Wait for children to finish too if this is a daemon that forks - # and if the daemon is only ever run from this initscript. - start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON - [ "$?" = 2 ] && return 2 - # Many daemons don't delete their pidfiles when they exit. - rm -f $PIDFILE - return "$RETVAL" + start-stop-daemon --stop --quiet --retry=INT/30/KILL/5 --pidfile \$PIDFILE --name stalwart + RETVAL="\$?" + [ "\$RETVAL" = 2 ] && return 2 + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec \$DAEMON + [ "\$?" = 2 ] && return 2 + rm -f \$PIDFILE + return "\$RETVAL" } -case "$1" in +case "\$1" in start) - [ "$VERBOSE" != no ] && log_daemon_msg "Starting Stalwart Server" "stalwart" + [ "\$VERBOSE" != no ] && log_daemon_msg "Starting Stalwart Server" "stalwart" do_start - case "$?" in - 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + case "\$?" in + 0|1) [ "\$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "\$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) - [ "$VERBOSE" != no ] && log_daemon_msg "Stopping Stalwart Server" "stalwart" + [ "\$VERBOSE" != no ] && log_daemon_msg "Stopping Stalwart Server" "stalwart" do_stop - case "$?" in - 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; - 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + case "\$?" in + 0|1) [ "\$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "\$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) - status_of_proc "$DAEMON" "stalwart" && exit 0 || exit $? + status_of_proc "\$DAEMON" "stalwart" && exit 0 || exit \$? ;; restart) log_daemon_msg "Restarting Stalwart Server" "stalwart" do_stop - case "$?" in + case "\$?" in 0|1) do_start - case "$?" in + case "\$?" in 0) log_end_msg 0 ;; - 1) log_end_msg 1 ;; # Old process is still running - *) log_end_msg 1 ;; # Failed to start + *) log_end_msg 1 ;; esac ;; *) - # Failed to stop log_end_msg 1 ;; esac @@ -290,34 +395,40 @@ esac exit 0 EOF chmod +x /etc/init.d/stalwart - - cat < /etc/default/stalwart -# Configuration for Stalwart init script being run during -# the boot sequence - -# Set to 'yes' to enable additional verbosity -#VERBOSE=no -EOF update-rc.d stalwart defaults service stalwart start } create_service_macos() { - local _dir="$1" - cat < /Library/LaunchAgents/stalwart.mail.plist + local _bin="$1" _config="$2" _env="$3" _user="$4" + local _plist="/Library/LaunchDaemons/stalwart.plist" + + # Remove any legacy LaunchAgent from a prior install + if [ -f /Library/LaunchAgents/stalwart.mail.plist ]; then + launchctl unload /Library/LaunchAgents/stalwart.mail.plist 2>/dev/null || true + rm -f /Library/LaunchAgents/stalwart.mail.plist + fi + + # launchd has no EnvironmentFile equivalent — wrap with sh to source the env file + cat > "$_plist" < Label - stalwart.mail + stalwart ServiceDescription Stalwart + UserName + ${_user} + GroupName + ${_user} ProgramArguments - __PATH__/bin/stalwart - --config=__PATH__/etc/config.toml + /bin/sh + -c + set -a; if [ -r "${_env}" ]; then . "${_env}"; fi; set +a; exec "${_bin}" --config="${_config}" RunAtLoad @@ -326,9 +437,11 @@ create_service_macos() { EOF - launchctl load /Library/LaunchAgents/stalwart.mail.plist - launchctl enable system/stalwart.mail - launchctl start system/stalwart.mail + chmod 0644 "$_plist" + chown root:wheel "$_plist" + launchctl bootout system "$_plist" 2>/dev/null || true + launchctl bootstrap system "$_plist" + launchctl enable system/stalwart } diff --git a/resources/docker/Dockerfile.fdb b/resources/docker/Dockerfile.fdb index 7fd3a811..a5aa20b6 100644 --- a/resources/docker/Dockerfile.fdb +++ b/resources/docker/Dockerfile.fdb @@ -49,4 +49,4 @@ RUN useradd stalwart -s /sbin/nologin -M RUN mkdir -p /opt/stalwart RUN chown stalwart:stalwart /opt/stalwart -ENTRYPOINT ["/usr/local/bin/stalwart", "--config", "/opt/stalwart/etc/config.toml"] +ENTRYPOINT ["/usr/local/bin/stalwart", "--config", "/opt/stalwart/etc/config.json"] diff --git a/resources/docker/entrypoint.sh b/resources/docker/entrypoint.sh index f98f7644..a845de1d 100644 --- a/resources/docker/entrypoint.sh +++ b/resources/docker/entrypoint.sh @@ -1,5 +1,4 @@ #!/usr/bin/env sh # shellcheck shell=dash -# If the configuration file exists, start the server. exec /usr/local/bin/stalwart --config /opt/stalwart/etc/config.json diff --git a/resources/systemd/stalwart-mail.service b/resources/systemd/stalwart-mail.service index 2b806d4e..4e2cb463 100644 --- a/resources/systemd/stalwart-mail.service +++ b/resources/systemd/stalwart-mail.service @@ -1,7 +1,7 @@ [Unit] Description=Stalwart Server Conflicts=postfix.service sendmail.service exim4.service -ConditionPathExists=__PATH__/etc/config.toml +ConditionPathExists=__PATH__/etc/config.json After=network-online.target [Service] @@ -11,7 +11,7 @@ KillMode=process KillSignal=SIGINT Restart=on-failure RestartSec=5 -ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.toml +ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.json SyslogIdentifier=stalwart User=stalwart Group=stalwart diff --git a/resources/systemd/stalwart.mail.plist b/resources/systemd/stalwart.mail.plist index 8fed0a6a..15f5ab1e 100644 --- a/resources/systemd/stalwart.mail.plist +++ b/resources/systemd/stalwart.mail.plist @@ -10,7 +10,7 @@ ProgramArguments __PATH__/bin/stalwart - --config=__PATH__/etc/config.toml + --config=__PATH__/etc/config.json RunAtLoad