Update install.sh
This commit is contained in:
@@ -14,17 +14,12 @@ use crate::{
|
|||||||
manager::defaults::BootstrapDefaults,
|
manager::defaults::BootstrapDefaults,
|
||||||
};
|
};
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use pwhash::sha512_crypt;
|
|
||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use store::{
|
use store::{RegistryStore, registry::bootstrap::Bootstrap};
|
||||||
RegistryStore,
|
|
||||||
rand::{Rng, distr::Alphanumeric, rng},
|
|
||||||
registry::bootstrap::Bootstrap,
|
|
||||||
};
|
|
||||||
use tokio::sync::{Notify, mpsc};
|
use tokio::sync::{Notify, mpsc};
|
||||||
use utils::{UnwrapFailure, failed};
|
use utils::{UnwrapFailure, failed};
|
||||||
|
|
||||||
@@ -54,7 +49,6 @@ Options:
|
|||||||
-e, --export <PATH> Export all store data to a specific path
|
-e, --export <PATH> Export all store data to a specific path
|
||||||
-i, --import <PATH> Import store data from a specific path
|
-i, --import <PATH> Import store data from a specific path
|
||||||
-o, --console Open the store console
|
-o, --console Open the store console
|
||||||
-I, --init <PATH> Initialize a new server at a specific path
|
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
-V, --version Print version
|
-V, --version Print version
|
||||||
"#
|
"#
|
||||||
@@ -99,10 +93,6 @@ impl BootManager {
|
|||||||
("config" | "c", Some(value)) => {
|
("config" | "c", Some(value)) => {
|
||||||
config_path = Some(value);
|
config_path = Some(value);
|
||||||
}
|
}
|
||||||
("init" | "I", Some(value)) => {
|
|
||||||
quickstart(value);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
("export" | "e", Some(value)) => {
|
("export" | "e", Some(value)) => {
|
||||||
import_export = StoreOp::Export(BackupParams::new(value.into()));
|
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_"
|
|
||||||
"#;
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::{
|
|||||||
now,
|
now,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use rand::{Rng, distr::Alphanumeric, rng};
|
||||||
use registry::{
|
use registry::{
|
||||||
schema::{enums::ClusterNodeStatus, structs::ClusterNode},
|
schema::{enums::ClusterNodeStatus, structs::ClusterNode},
|
||||||
types::datetime::UTCDateTime,
|
types::datetime::UTCDateTime,
|
||||||
@@ -36,6 +37,33 @@ impl RegistryStore {
|
|||||||
RegistryInit::Err(err) => return Err(err),
|
RegistryInit::Err(err) => return Err(err),
|
||||||
RegistryInit::Bootstrap => {
|
RegistryInit::Bootstrap => {
|
||||||
inner.env_recovery_mode = true;
|
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()
|
EphemeralStore::open()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,15 +26,13 @@ impl RegistryStoreInner {
|
|||||||
.ok()
|
.ok()
|
||||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
env_recovery_admin: std::env::var("STALWART_ADMIN_ACCOUNT")
|
env_recovery_admin: std::env::var("STALWART_RECOVERY_ADMIN")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|a| !a.is_empty())
|
.and_then(|v| {
|
||||||
.and_then(|a| {
|
v.split_once(':')
|
||||||
std::env::var("STALWART_ADMIN_PASS")
|
.map(|(a, p)| (a.trim().to_string(), p.trim().to_string()))
|
||||||
.ok()
|
})
|
||||||
.filter(|p| !p.is_empty())
|
.filter(|(a, p)| !a.is_empty() && !p.is_empty()),
|
||||||
.map(|p| (a, p))
|
|
||||||
}),
|
|
||||||
env_cluster_role: std::env::var("STALWART_ROLE")
|
env_cluster_role: std::env::var("STALWART_ROLE")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|r| !r.is_empty()),
|
.filter(|r| !r.is_empty()),
|
||||||
|
|||||||
431
install.sh
431
install.sh
@@ -19,136 +19,256 @@ main() {
|
|||||||
need_cmd uname
|
need_cmd uname
|
||||||
need_cmd mktemp
|
need_cmd mktemp
|
||||||
need_cmd chmod
|
need_cmd chmod
|
||||||
|
need_cmd chown
|
||||||
need_cmd mkdir
|
need_cmd mkdir
|
||||||
need_cmd rm
|
need_cmd rm
|
||||||
need_cmd rmdir
|
|
||||||
need_cmd tar
|
need_cmd tar
|
||||||
|
need_cmd cp
|
||||||
|
need_cmd hostname
|
||||||
|
|
||||||
# Make sure we are running as root
|
# Require root
|
||||||
if [ "$(id -u)" -ne 0 ] ; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
err "❌ Install failed: This program needs to run as root."
|
err "❌ Install failed: This program needs to run as root."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect OS
|
# Detect OS
|
||||||
local _os="unknown"
|
local _os _uname _account
|
||||||
local _uname="$(uname)"
|
_uname="$(uname)"
|
||||||
_account="stalwart"
|
_account="stalwart"
|
||||||
if [ "${_uname}" = "Linux" ]; then
|
case "$_uname" in
|
||||||
_os="linux"
|
Linux) _os="linux" ;;
|
||||||
elif [ "${_uname}" = "Darwin" ]; then
|
Darwin) _os="macos"; _account="_stalwart" ;;
|
||||||
_os="macos"
|
*) err "❌ Install failed: Unsupported OS: $_uname" ;;
|
||||||
_account="_stalwart"
|
esac
|
||||||
fi
|
|
||||||
|
|
||||||
# Read arguments
|
# Parse arguments
|
||||||
local _dir="/opt/stalwart"
|
|
||||||
|
|
||||||
# Default component setting
|
|
||||||
local _component="stalwart"
|
local _component="stalwart"
|
||||||
|
local _prefix=""
|
||||||
# Loop through the arguments
|
while [ $# -gt 0 ]; do
|
||||||
for arg in "$@"; do
|
case "$1" in
|
||||||
case "$arg" in
|
|
||||||
--fdb)
|
--fdb)
|
||||||
_component="stalwart-foundationdb"
|
_component="stalwart-foundationdb"
|
||||||
;;
|
;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--*|-*)
|
||||||
|
err "❌ Unknown flag: $1 (try --help)"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
if [ -n "$arg" ]; then
|
if [ -n "$_prefix" ]; then
|
||||||
_dir=$arg
|
err "❌ Only one prefix argument is allowed, got: $_prefix $1"
|
||||||
fi
|
fi
|
||||||
|
_prefix="$1"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
shift
|
||||||
done
|
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
|
get_architecture || return 1
|
||||||
local _arch="$RETVAL"
|
local _arch="$RETVAL"
|
||||||
assert_nz "$_arch" "arch"
|
assert_nz "$_arch" "arch"
|
||||||
|
|
||||||
|
# Create service account
|
||||||
|
create_account "$_os" "$_account"
|
||||||
|
|
||||||
# Create directories
|
# 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}..."
|
say "⏳ Downloading ${_component} for ${_arch}..."
|
||||||
local _file="${_dir}/bin/stalwart.tar.gz"
|
local _tmp _tar _src_name
|
||||||
local _url="${BASE_URL}/${_component}-${_arch}.tar.gz"
|
_tmp="$(mktemp -d)"
|
||||||
ensure mkdir -p "$_dir"
|
_tar="${_tmp}/stalwart.tar.gz"
|
||||||
ensure downloader "$_url" "$_file" "$_arch"
|
ensure downloader "${BASE_URL}/${_component}-${_arch}.tar.gz" "$_tar" "$_arch"
|
||||||
ensure tar zxvf "$_file" -C "$_dir/bin"
|
ensure tar zxf "$_tar" -C "$_tmp"
|
||||||
|
_src_name="stalwart"
|
||||||
if [ "$_component" = "stalwart-foundationdb" ]; then
|
if [ "$_component" = "stalwart-foundationdb" ]; then
|
||||||
ignore mv "$_dir/bin/stalwart-foundationdb" "$_dir/bin/stalwart"
|
_src_name="stalwart-foundationdb"
|
||||||
fi
|
fi
|
||||||
ignore chmod +x "$_dir/bin/stalwart"
|
ensure cp "${_tmp}/${_src_name}" "$_bin_file"
|
||||||
ignore rm "$_file"
|
ensure chmod 0755 "$_bin_file"
|
||||||
|
ensure rm -rf "$_tmp"
|
||||||
|
|
||||||
# Create system account
|
# Create env file if absent (preserve user edits on reinstall)
|
||||||
if ! id -u ${_account} > /dev/null 2>&1; then
|
if [ ! -e "$_env_file" ]; then
|
||||||
say "🖥️ Creating '${_account}' account..."
|
say "📝 Writing env file at ${_env_file}..."
|
||||||
if [ "${_os}" = "macos" ]; then
|
write_env_file "$_env_file"
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run init
|
# Ownership and permissions
|
||||||
ignore $_dir/bin/stalwart --init "$_dir"
|
|
||||||
|
|
||||||
# Set permissions
|
|
||||||
say "🔐 Setting permissions..."
|
say "🔐 Setting permissions..."
|
||||||
ensure chown -R ${_account}:${_account} "$_dir"
|
ensure chown "${_account}:${_account}" "$_conf_dir" "$_log_dir" "$_data_dir"
|
||||||
ensure chmod -R 755 "$_dir"
|
ensure chmod 0750 "$_conf_dir" "$_log_dir" "$_data_dir"
|
||||||
ensure chmod 700 "$_dir/etc/config.toml"
|
ensure chown "root:${_account}" "$_env_file"
|
||||||
|
ensure chmod 0640 "$_env_file"
|
||||||
|
|
||||||
# Create service file
|
# Install and start the service
|
||||||
say "🚀 Starting service..."
|
say "🚀 Starting service..."
|
||||||
if [ "${_os}" = "linux" ]; then
|
local _service_type=""
|
||||||
local _issystemdlinux=$(command -v systemctl)
|
case "$_os" in
|
||||||
if [ -n "$_issystemdlinux" ]; then
|
linux)
|
||||||
create_service_linux_systemd "$_dir"
|
if check_cmd systemctl; then
|
||||||
else
|
create_service_linux_systemd "$_bin_file" "$_config_file" "$_env_file" "$_account"
|
||||||
create_service_linux_initd "$_dir"
|
_service_type="systemd"
|
||||||
fi
|
else
|
||||||
elif [ "${_os}" = "macos" ]; then
|
create_service_linux_initd "$_bin_file" "$_config_file" "$_env_file" "$_account"
|
||||||
create_service_macos "$_dir"
|
_service_type="initd"
|
||||||
fi
|
fi
|
||||||
|
;;
|
||||||
|
macos)
|
||||||
|
create_service_macos "$_bin_file" "$_config_file" "$_env_file" "$_account"
|
||||||
|
_service_type="launchd"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Installation complete
|
# Completion message
|
||||||
local _host=$(hostname -f)
|
local _host
|
||||||
say "🎉 Installation complete! Continue the setup at http://$_host:8080/login"
|
_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
|
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() {
|
create_service_linux_systemd() {
|
||||||
local _dir="$1"
|
local _bin="$1" _config="$2" _env="$3" _user="$4"
|
||||||
cat <<EOF | sed "s|__PATH__|$_dir|g" > /etc/systemd/system/stalwart.service
|
cat > /etc/systemd/system/stalwart.service <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Stalwart
|
Description=Stalwart
|
||||||
Conflicts=postfix.service sendmail.service exim4.service
|
Conflicts=postfix.service sendmail.service exim4.service
|
||||||
ConditionPathExists=__PATH__/etc/config.toml
|
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -158,10 +278,11 @@ KillMode=process
|
|||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.toml
|
EnvironmentFile=-${_env}
|
||||||
|
ExecStart=${_bin} --config=${_config}
|
||||||
SyslogIdentifier=stalwart
|
SyslogIdentifier=stalwart
|
||||||
User=stalwart
|
User=${_user}
|
||||||
Group=stalwart
|
Group=${_user}
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
@@ -173,13 +294,13 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
create_service_linux_initd() {
|
create_service_linux_initd() {
|
||||||
local _dir="$1"
|
local _bin="$1" _config="$2" _env="$3" _user="$4"
|
||||||
cat <<"EOF" | sed "s|__PATH__|$_dir|g" > /etc/init.d/stalwart
|
cat > /etc/init.d/stalwart <<EOF
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
### BEGIN INIT INFO
|
### BEGIN INIT INFO
|
||||||
# Provides: stalwart
|
# Provides: stalwart
|
||||||
# Required-Start: $network
|
# Required-Start: \$network
|
||||||
# Required-Stop: $network
|
# Required-Stop: \$network
|
||||||
# Default-Start: 2 3 4 5
|
# Default-Start: 2 3 4 5
|
||||||
# Default-Stop: 0 1 6
|
# Default-Stop: 0 1 6
|
||||||
# Short-Description: Stalwart Server
|
# Short-Description: Stalwart Server
|
||||||
@@ -192,91 +313,75 @@ PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
|||||||
. /lib/init/vars.sh
|
. /lib/init/vars.sh
|
||||||
. /lib/lsb/init-functions
|
. /lib/lsb/init-functions
|
||||||
|
|
||||||
# Service Config
|
DAEMON=${_bin}
|
||||||
DAEMON=__PATH__/bin/stalwart
|
DAEMON_ARGS="--config=${_config}"
|
||||||
DAEMON_ARGS="--config=__PATH__/etc/config.toml"
|
ENV_FILE=${_env}
|
||||||
PIDFILE=/var/run/stalwart.pid
|
PIDFILE=/var/run/stalwart.pid
|
||||||
ULIMIT_NOFILE=65536
|
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
|
if [ -r "\$ENV_FILE" ]; then
|
||||||
[ -f "__PATH__/etc/config.toml" ] || exit 0
|
set -a
|
||||||
|
. "\$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
# Read configuration variable file if it is present
|
ulimit -n \$ULIMIT_NOFILE
|
||||||
[ -r /etc/default/stalwart ] && . /etc/default/stalwart
|
|
||||||
|
|
||||||
# Increase file descriptor limit
|
|
||||||
ulimit -n $ULIMIT_NOFILE
|
|
||||||
|
|
||||||
do_start()
|
do_start()
|
||||||
{
|
{
|
||||||
# Return
|
start-stop-daemon --start --quiet --pidfile \$PIDFILE --exec \$DAEMON --test > /dev/null \\
|
||||||
# 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 \
|
|
||||||
|| return 1
|
|| return 1
|
||||||
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \
|
start-stop-daemon --start --quiet --pidfile \$PIDFILE --exec \$DAEMON \\
|
||||||
--background --make-pidfile --chuid stalwart:stalwart \
|
--background --make-pidfile --chuid ${_user}:${_user} \\
|
||||||
-- $DAEMON_ARGS \
|
-- \$DAEMON_ARGS \\
|
||||||
|| return 2
|
|| return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
do_stop()
|
do_stop()
|
||||||
{
|
{
|
||||||
# Return
|
start-stop-daemon --stop --quiet --retry=INT/30/KILL/5 --pidfile \$PIDFILE --name stalwart
|
||||||
# 0 if daemon has been stopped
|
RETVAL="\$?"
|
||||||
# 1 if daemon was already stopped
|
[ "\$RETVAL" = 2 ] && return 2
|
||||||
# 2 if daemon could not be stopped
|
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec \$DAEMON
|
||||||
# other if a failure occurred
|
[ "\$?" = 2 ] && return 2
|
||||||
start-stop-daemon --stop --quiet --retry=INT/30/KILL/5 --pidfile $PIDFILE --name stalwart
|
rm -f \$PIDFILE
|
||||||
RETVAL="$?"
|
return "\$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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$1" in
|
case "\$1" in
|
||||||
start)
|
start)
|
||||||
[ "$VERBOSE" != no ] && log_daemon_msg "Starting Stalwart Server" "stalwart"
|
[ "\$VERBOSE" != no ] && log_daemon_msg "Starting Stalwart Server" "stalwart"
|
||||||
do_start
|
do_start
|
||||||
case "$?" in
|
case "\$?" in
|
||||||
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
0|1) [ "\$VERBOSE" != no ] && log_end_msg 0 ;;
|
||||||
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
2) [ "\$VERBOSE" != no ] && log_end_msg 1 ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping Stalwart Server" "stalwart"
|
[ "\$VERBOSE" != no ] && log_daemon_msg "Stopping Stalwart Server" "stalwart"
|
||||||
do_stop
|
do_stop
|
||||||
case "$?" in
|
case "\$?" in
|
||||||
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
0|1) [ "\$VERBOSE" != no ] && log_end_msg 0 ;;
|
||||||
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
2) [ "\$VERBOSE" != no ] && log_end_msg 1 ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
status_of_proc "$DAEMON" "stalwart" && exit 0 || exit $?
|
status_of_proc "\$DAEMON" "stalwart" && exit 0 || exit \$?
|
||||||
;;
|
;;
|
||||||
restart)
|
restart)
|
||||||
log_daemon_msg "Restarting Stalwart Server" "stalwart"
|
log_daemon_msg "Restarting Stalwart Server" "stalwart"
|
||||||
do_stop
|
do_stop
|
||||||
case "$?" in
|
case "\$?" in
|
||||||
0|1)
|
0|1)
|
||||||
do_start
|
do_start
|
||||||
case "$?" in
|
case "\$?" in
|
||||||
0) log_end_msg 0 ;;
|
0) log_end_msg 0 ;;
|
||||||
1) log_end_msg 1 ;; # Old process is still running
|
*) log_end_msg 1 ;;
|
||||||
*) log_end_msg 1 ;; # Failed to start
|
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
# Failed to stop
|
|
||||||
log_end_msg 1
|
log_end_msg 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -290,34 +395,40 @@ esac
|
|||||||
exit 0
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
chmod +x /etc/init.d/stalwart
|
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
|
update-rc.d stalwart defaults
|
||||||
service stalwart start
|
service stalwart start
|
||||||
}
|
}
|
||||||
|
|
||||||
create_service_macos() {
|
create_service_macos() {
|
||||||
local _dir="$1"
|
local _bin="$1" _config="$2" _env="$3" _user="$4"
|
||||||
cat <<EOF | sed "s|__PATH__|$_dir|g" > /Library/LaunchAgents/stalwart.mail.plist
|
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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
|
||||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>stalwart.mail</string>
|
<string>stalwart</string>
|
||||||
<key>ServiceDescription</key>
|
<key>ServiceDescription</key>
|
||||||
<string>Stalwart</string>
|
<string>Stalwart</string>
|
||||||
|
<key>UserName</key>
|
||||||
|
<string>${_user}</string>
|
||||||
|
<key>GroupName</key>
|
||||||
|
<string>${_user}</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>__PATH__/bin/stalwart</string>
|
<string>/bin/sh</string>
|
||||||
<string>--config=__PATH__/etc/config.toml</string>
|
<string>-c</string>
|
||||||
|
<string>set -a; if [ -r "${_env}" ]; then . "${_env}"; fi; set +a; exec "${_bin}" --config="${_config}"</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -326,9 +437,11 @@ create_service_macos() {
|
|||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
launchctl load /Library/LaunchAgents/stalwart.mail.plist
|
chmod 0644 "$_plist"
|
||||||
launchctl enable system/stalwart.mail
|
chown root:wheel "$_plist"
|
||||||
launchctl start system/stalwart.mail
|
launchctl bootout system "$_plist" 2>/dev/null || true
|
||||||
|
launchctl bootstrap system "$_plist"
|
||||||
|
launchctl enable system/stalwart
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,4 +49,4 @@ RUN useradd stalwart -s /sbin/nologin -M
|
|||||||
RUN mkdir -p /opt/stalwart
|
RUN mkdir -p /opt/stalwart
|
||||||
RUN chown stalwart:stalwart /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"]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# shellcheck shell=dash
|
# shellcheck shell=dash
|
||||||
|
|
||||||
# If the configuration file exists, start the server.
|
|
||||||
exec /usr/local/bin/stalwart --config /opt/stalwart/etc/config.json
|
exec /usr/local/bin/stalwart --config /opt/stalwart/etc/config.json
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Stalwart Server
|
Description=Stalwart Server
|
||||||
Conflicts=postfix.service sendmail.service exim4.service
|
Conflicts=postfix.service sendmail.service exim4.service
|
||||||
ConditionPathExists=__PATH__/etc/config.toml
|
ConditionPathExists=__PATH__/etc/config.json
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -11,7 +11,7 @@ KillMode=process
|
|||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.toml
|
ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.json
|
||||||
SyslogIdentifier=stalwart
|
SyslogIdentifier=stalwart
|
||||||
User=stalwart
|
User=stalwart
|
||||||
Group=stalwart
|
Group=stalwart
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>__PATH__/bin/stalwart</string>
|
<string>__PATH__/bin/stalwart</string>
|
||||||
<string>--config=__PATH__/etc/config.toml</string>
|
<string>--config=__PATH__/etc/config.json</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
Reference in New Issue
Block a user