Update install.sh

This commit is contained in:
Maurus Decimus
2026-04-19 09:30:35 +02:00
parent af23099e35
commit e3399a2a22
8 changed files with 311 additions and 369 deletions

View File

@@ -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 <PATH> Export all store data to a specific path
-i, --import <PATH> Import store data from a specific path
-o, --console Open the store console
-I, --init <PATH> 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<PathBuf>) {
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::<String>()
});
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_"
"#;

View File

@@ -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::<String>();
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:<password> in the");
eprintln!("env file.");
eprintln!("════════════════════════════════════════════════════════════");
eprintln!();
inner.env_recovery_admin = Some(("admin".to_string(), password));
}
EphemeralStore::open()
}
};

View File

@@ -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()),

View File

@@ -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:<password> 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 <<EOF | sed "s|__PATH__|$_dir|g" > /etc/systemd/system/stalwart.service
local _bin="$1" _config="$2" _env="$3" _user="$4"
cat > /etc/systemd/system/stalwart.service <<EOF
[Unit]
Description=Stalwart
Conflicts=postfix.service sendmail.service exim4.service
ConditionPathExists=__PATH__/etc/config.toml
After=network-online.target
[Service]
@@ -158,10 +278,11 @@ KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.toml
EnvironmentFile=-${_env}
ExecStart=${_bin} --config=${_config}
SyslogIdentifier=stalwart
User=stalwart
Group=stalwart
User=${_user}
Group=${_user}
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
@@ -173,13 +294,13 @@ EOF
}
create_service_linux_initd() {
local _dir="$1"
cat <<"EOF" | sed "s|__PATH__|$_dir|g" > /etc/init.d/stalwart
local _bin="$1" _config="$2" _env="$3" _user="$4"
cat > /etc/init.d/stalwart <<EOF
#!/bin/sh
### BEGIN INIT INFO
# Provides: stalwart
# Required-Start: $network
# Required-Stop: $network
# Required-Start: \$network
# Required-Stop: \$network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Stalwart Server
@@ -192,91 +313,75 @@ PATH=/sbin:/usr/sbin:/bin:/usr/bin
. /lib/init/vars.sh
. /lib/lsb/init-functions
# Service Config
DAEMON=__PATH__/bin/stalwart
DAEMON_ARGS="--config=__PATH__/etc/config.toml"
DAEMON=${_bin}
DAEMON_ARGS="--config=${_config}"
ENV_FILE=${_env}
PIDFILE=/var/run/stalwart.pid
ULIMIT_NOFILE=65536
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0
[ -x "\$DAEMON" ] || exit 0
# Exit if config file doesn't exist
[ -f "__PATH__/etc/config.toml" ] || exit 0
if [ -r "\$ENV_FILE" ]; then
set -a
. "\$ENV_FILE"
set +a
fi
# Read configuration variable file if it is present
[ -r /etc/default/stalwart ] && . /etc/default/stalwart
# Increase file descriptor limit
ulimit -n $ULIMIT_NOFILE
ulimit -n \$ULIMIT_NOFILE
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /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 <<EOF > /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 <<EOF | sed "s|__PATH__|$_dir|g" > /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" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>stalwart.mail</string>
<string>stalwart</string>
<key>ServiceDescription</key>
<string>Stalwart</string>
<key>UserName</key>
<string>${_user}</string>
<key>GroupName</key>
<string>${_user}</string>
<key>ProgramArguments</key>
<array>
<string>__PATH__/bin/stalwart</string>
<string>--config=__PATH__/etc/config.toml</string>
<string>/bin/sh</string>
<string>-c</string>
<string>set -a; if [ -r "${_env}" ]; then . "${_env}"; fi; set +a; exec "${_bin}" --config="${_config}"</string>
</array>
<key>RunAtLoad</key>
<true/>
@@ -326,9 +437,11 @@ create_service_macos() {
</dict>
</plist>
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
}

View File

@@ -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"]

View File

@@ -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

View File

@@ -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

View File

@@ -10,7 +10,7 @@
<key>ProgramArguments</key>
<array>
<string>__PATH__/bin/stalwart</string>
<string>--config=__PATH__/etc/config.toml</string>
<string>--config=__PATH__/etc/config.json</string>
</array>
<key>RunAtLoad</key>
<true/>