diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 12091e7e..c0a8fdfe 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -59,10 +59,10 @@ body: label: Stalwart Version description: What version of Stalwart are you running? options: + - v0.16.x - v0.15.x - v0.14.x - - v0.13.x - - v0.12.x or lower + - v0.13.x or lower validations: required: true - type: dropdown diff --git a/crates/common/src/config/mailstore/scripts.rs b/crates/common/src/config/mailstore/scripts.rs index 2f75fd2a..57e6c580 100644 --- a/crates/common/src/config/mailstore/scripts.rs +++ b/crates/common/src/config/mailstore/scripts.rs @@ -31,6 +31,7 @@ pub struct Scripting { pub untrusted_compiler: Compiler, pub untrusted_runtime: Runtime, pub trusted_runtime: Runtime, + pub trusted_compiler: Compiler, pub from_addr: IfBlock, pub from_name: IfBlock, pub return_path: IfBlock, @@ -176,6 +177,7 @@ impl Scripting { untrusted_compiler, untrusted_runtime, trusted_runtime, + trusted_compiler, untrusted_scripts, trusted_scripts, from_addr: bp.compile_expr( @@ -210,6 +212,7 @@ impl Clone for Scripting { sign: self.sign.clone(), trusted_scripts: self.trusted_scripts.clone(), untrusted_scripts: self.untrusted_scripts.clone(), + trusted_compiler: self.trusted_compiler.clone(), } } } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index b4971a69..9e880859 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use crate::auth::{AccessTokenInner, EmailAddress}; use crate::manager::application::WebApplications; diff --git a/crates/coordinator/src/lib.rs b/crates/coordinator/src/lib.rs index 9d9a5115..3abbde90 100644 --- a/crates/coordinator/src/lib.rs +++ b/crates/coordinator/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] #[allow(unused_imports)] use std::sync::Arc; diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 3adfc0ab..61b105d5 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod calendar; pub mod card; diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 3c576bde..c225df71 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use crate::backend::oidc::OpenIdDirectory; use backend::{ldap::LdapDirectory, sql::SqlDirectory}; diff --git a/crates/email/src/lib.rs b/crates/email/src/lib.rs index 42da9acb..815803d7 100644 --- a/crates/email/src/lib.rs +++ b/crates/email/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod cache; pub mod identity; diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index 0001a113..38602c22 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use calcard::common::timezone::Tz; use common::DavResources; diff --git a/crates/http-proto/src/lib.rs b/crates/http-proto/src/lib.rs index f4fffffb..628b5436 100644 --- a/crates/http-proto/src/lib.rs +++ b/crates/http-proto/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod context; pub mod request; diff --git a/crates/http/src/lib.rs b/crates/http/src/lib.rs index 10f30703..f5adc5b6 100644 --- a/crates/http/src/lib.rs +++ b/crates/http/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod api; pub mod auth; diff --git a/crates/imap/src/lib.rs b/crates/imap/src/lib.rs index 4a56ead0..b2a83d5e 100644 --- a/crates/imap/src/lib.rs +++ b/crates/imap/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use std::sync::LazyLock; diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 94414cb6..62fde961 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod addressbook; pub mod api; diff --git a/crates/jmap/src/registry/mapping/sieve.rs b/crates/jmap/src/registry/mapping/sieve.rs index 04aae17e..dbd5dd20 100644 --- a/crates/jmap/src/registry/mapping/sieve.rs +++ b/crates/jmap/src/registry/mapping/sieve.rs @@ -20,7 +20,7 @@ pub(crate) async fn validate_sieve_script( if let Err(err) = server .core .sieve - .untrusted_compiler + .trusted_compiler .compile(script.as_bytes()) { return Ok(Err(SetError::invalid_properties() diff --git a/crates/jmap/src/registry/query.rs b/crates/jmap/src/registry/query.rs index 732fa42c..14044c40 100644 --- a/crates/jmap/src/registry/query.rs +++ b/crates/jmap/src/registry/query.rs @@ -397,6 +397,15 @@ impl RegistryQueryFilters for QueryRequest { max_results: usize, external_filter: Option, ) -> trc::Result { + #[cfg(feature = "test_mode")] + let comparator = self + .sort + .take() + .unwrap_or_default() + .into_iter() + .next() + .unwrap_or_else(|| Comparator::ascending(RegistryComparator::Property(Property::Id))); + #[cfg(not(feature = "test_mode"))] let comparator = self .sort .take() diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 3c77bea3..d71e341e 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] #![warn(clippy::cast_possible_truncation)] #![warn(clippy::cast_possible_wrap)] #![warn(clippy::cast_sign_loss)] diff --git a/crates/managesieve/src/lib.rs b/crates/managesieve/src/lib.rs index fb665dff..0237a695 100644 --- a/crates/managesieve/src/lib.rs +++ b/crates/managesieve/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod core; pub mod op; diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 483b59ef..3066afb8 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use crate::v016::migrate_v0_16; use common::{DATABASE_SCHEMA_VERSION, Server}; diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs index 3ca6c6d0..d800fd7e 100644 --- a/crates/pop3/src/lib.rs +++ b/crates/pop3/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use std::{net::IpAddr, sync::Arc}; diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index 1710fae9..0772aef0 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod jmap; pub mod pickle; diff --git a/crates/registry/src/schema/properties.rs b/crates/registry/src/schema/properties.rs index 8cc4cb91..864549d8 100644 --- a/crates/registry/src/schema/properties.rs +++ b/crates/registry/src/schema/properties.rs @@ -447,7 +447,6 @@ pub enum Property { Directory = 12, DirectoryId = 104, DisableCapabilities = 711, - DisableLanguages = 666, DisabledPermissions = 629, DiscardAfter = 872, Disposition = 747, @@ -1047,6 +1046,7 @@ pub enum Property { Subscribe = 368, Sum = 494, Summary = 808, + SupportedLanguages = 666, Tag = 748, Tags = 746, TaskTypes = 189, diff --git a/crates/registry/src/schema/properties_impl.rs b/crates/registry/src/schema/properties_impl.rs index 603f9e6f..b9b3d500 100644 --- a/crates/registry/src/schema/properties_impl.rs +++ b/crates/registry/src/schema/properties_impl.rs @@ -600,7 +600,6 @@ impl EnumImpl for Property { b"directory" => Property::Directory, b"directoryId" => Property::DirectoryId, b"disableCapabilities" => Property::DisableCapabilities, - b"disableLanguages" => Property::DisableLanguages, b"disabledPermissions" => Property::DisabledPermissions, b"discardAfter" => Property::DiscardAfter, b"disposition" => Property::Disposition, @@ -1200,6 +1199,7 @@ impl EnumImpl for Property { b"subscribe" => Property::Subscribe, b"sum" => Property::Sum, b"summary" => Property::Summary, + b"supportedLanguages" => Property::SupportedLanguages, b"tag" => Property::Tag, b"tags" => Property::Tags, b"taskTypes" => Property::TaskTypes, @@ -1479,7 +1479,6 @@ impl EnumImpl for Property { Property::Directory => "directory", Property::DirectoryId => "directoryId", Property::DisableCapabilities => "disableCapabilities", - Property::DisableLanguages => "disableLanguages", Property::DisabledPermissions => "disabledPermissions", Property::DiscardAfter => "discardAfter", Property::Disposition => "disposition", @@ -2079,6 +2078,7 @@ impl EnumImpl for Property { Property::Subscribe => "subscribe", Property::Sum => "sum", Property::Summary => "summary", + Property::SupportedLanguages => "supportedLanguages", Property::Tag => "tag", Property::Tags => "tags", Property::TaskTypes => "taskTypes", @@ -2362,7 +2362,6 @@ impl EnumImpl for Property { 12 => Some(Property::Directory), 104 => Some(Property::DirectoryId), 711 => Some(Property::DisableCapabilities), - 666 => Some(Property::DisableLanguages), 629 => Some(Property::DisabledPermissions), 872 => Some(Property::DiscardAfter), 747 => Some(Property::Disposition), @@ -2962,6 +2961,7 @@ impl EnumImpl for Property { 368 => Some(Property::Subscribe), 494 => Some(Property::Sum), 808 => Some(Property::Summary), + 666 => Some(Property::SupportedLanguages), 748 => Some(Property::Tag), 746 => Some(Property::Tags), 189 => Some(Property::TaskTypes), diff --git a/crates/registry/src/schema/structs.rs b/crates/registry/src/schema/structs.rs index 24642375..f974651e 100644 --- a/crates/registry/src/schema/structs.rs +++ b/crates/registry/src/schema/structs.rs @@ -3436,8 +3436,8 @@ pub struct Search { pub index_batch_size: u64, #[serde(rename = "defaultLanguage")] pub default_language: Locale, - #[serde(rename = "disableLanguages")] - pub disable_languages: Map, + #[serde(rename = "supportedLanguages")] + pub supported_languages: Map, #[serde(rename = "indexCalendar")] pub index_calendar: bool, #[serde(rename = "indexCalendarFields")] diff --git a/crates/registry/src/schema/structs_impl.rs b/crates/registry/src/schema/structs_impl.rs index 25c04a99..fe0d334a 100644 --- a/crates/registry/src/schema/structs_impl.rs +++ b/crates/registry/src/schema/structs_impl.rs @@ -25923,7 +25923,7 @@ impl Pickle for Search { fn pickle(&self, out: &mut Vec) { self.index_batch_size.pickle(out); self.default_language.pickle(out); - self.disable_languages.pickle(out); + self.supported_languages.pickle(out); self.index_calendar.pickle(out); self.index_calendar_fields.pickle(out); self.index_contacts.pickle(out); @@ -25938,7 +25938,7 @@ impl Pickle for Search { let mut this = Self::default(); this.index_batch_size = Pickle::unpickle(stream)?; this.default_language = Pickle::unpickle(stream)?; - this.disable_languages = Pickle::unpickle(stream)?; + this.supported_languages = Pickle::unpickle(stream)?; this.index_calendar = Pickle::unpickle(stream)?; this.index_calendar_fields = Pickle::unpickle(stream)?; this.index_contacts = Pickle::unpickle(stream)?; @@ -25956,7 +25956,7 @@ impl Default for Search { Self { index_batch_size: 100u64, default_language: Locale::EnUS, - disable_languages: Default::default(), + supported_languages: Map::new(vec![Locale::EnUS]), index_calendar: true, index_calendar_fields: Map::new(vec![ SearchCalendarField::Title, @@ -26014,8 +26014,8 @@ impl IntoValue for Search { self.default_language.into_value(), ); map.insert_unchecked( - Property::DisableLanguages, - self.disable_languages.into_value(), + Property::SupportedLanguages, + self.supported_languages.into_value(), ); map.insert_unchecked(Property::IndexCalendar, self.index_calendar.into_value()); map.insert_unchecked( @@ -26050,7 +26050,7 @@ impl RegistryJsonPropertyPatch for Search { match pointer.next_property() { Some(Property::IndexBatchSize) => self.index_batch_size.patch(pointer, value), Some(Property::DefaultLanguage) => self.default_language.patch(pointer, value), - Some(Property::DisableLanguages) => self.disable_languages.patch(pointer, value), + Some(Property::SupportedLanguages) => self.supported_languages.patch(pointer, value), Some(Property::IndexCalendar) => self.index_calendar.patch(pointer, value), Some(Property::IndexCalendarFields) => self.index_calendar_fields.patch(pointer, value), Some(Property::IndexContacts) => self.index_contacts.patch(pointer, value), diff --git a/crates/services/src/lib.rs b/crates/services/src/lib.rs index 9032d87b..b249b46b 100644 --- a/crates/services/src/lib.rs +++ b/crates/services/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use broadcast::publisher::spawn_broadcast_publisher; use common::{ diff --git a/crates/smtp/src/lib.rs b/crates/smtp/src/lib.rs index 1fde369d..bf1cb5b0 100644 --- a/crates/smtp/src/lib.rs +++ b/crates/smtp/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] use common::{ Inner, diff --git a/crates/spam-filter/src/lib.rs b/crates/spam-filter/src/lib.rs index b4ca6df2..8a6cbfce 100644 --- a/crates/spam-filter/src/lib.rs +++ b/crates/spam-filter/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod analysis; pub mod modules; diff --git a/crates/store/src/backend/postgres/lookup.rs b/crates/store/src/backend/postgres/lookup.rs index 71374798..30b052f9 100644 --- a/crates/store/src/backend/postgres/lookup.rs +++ b/crates/store/src/backend/postgres/lookup.rs @@ -33,10 +33,7 @@ impl PostgresStore { .await .map_or_else(|e| Err(into_error(e)), |r| Ok(T::from_exec(r as usize))), QueryType::Exists => { - let rows = conn - .query_raw(&s, params.into_iter()) - .await - .map_err(into_error)?; + let rows = conn.query_raw(&s, params).await.map_err(into_error)?; pin_mut!(rows); rows.try_next() .await diff --git a/crates/store/src/backend/postgres/search.rs b/crates/store/src/backend/postgres/search.rs index 866f254d..8c5405f7 100644 --- a/crates/store/src/backend/postgres/search.rs +++ b/crates/store/src/backend/postgres/search.rs @@ -432,7 +432,7 @@ fn pg_lang(lang: &Language) -> Option<&'static str> { Language::Japanese => None, Language::Hebrew => None, Language::Yiddish => Some("yiddish"), - Language::Polish => Some("polish"), + Language::Polish => None, Language::Amharic => None, Language::Javanese => None, Language::Korean => None, @@ -443,7 +443,7 @@ fn pg_lang(lang: &Language) -> Option<&'static str> { Language::Turkish => Some("turkish"), Language::Dutch => Some("dutch"), Language::Hungarian => Some("hungarian"), - Language::Czech => Some("czech"), + Language::Czech => None, Language::Greek => Some("greek"), Language::Bulgarian => None, Language::Belarusian => None, diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 18bbe6a5..9c3829f9 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod backend; pub mod build; diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index 70923db5..cefa91b1 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod atomics; pub mod event; diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 63476e94..69f6e20d 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#![deny(clippy::large_futures)] +#![warn(clippy::large_futures)] pub mod bimap; pub mod cache; diff --git a/install.sh b/install.sh index 4f6c8356..535bb732 100644 --- a/install.sh +++ b/install.sh @@ -19,136 +19,259 @@ 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. + +# Override the hostname used in HTTP responses +#STALWART_HOSTNAME=mail.example.com + +# 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 +398,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 +440,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/install.sh.new b/install.sh.new deleted file mode 100644 index 535bb732..00000000 --- a/install.sh.new +++ /dev/null @@ -1,993 +0,0 @@ -#!/usr/bin/env sh -# shellcheck shell=dash - -# -# SPDX-FileCopyrightText: 2020 Stalwart Labs LLC -# -# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL -# - -# Stalwart install script -- based on the rustup installation script. - -set -e -set -u - -readonly BASE_URL="https://github.com/stalwartlabs/stalwart/releases/latest/download" - -main() { - downloader --check - need_cmd uname - need_cmd mktemp - need_cmd chmod - need_cmd chown - need_cmd mkdir - need_cmd rm - need_cmd tar - need_cmd cp - need_cmd hostname - - # Require root - if [ "$(id -u)" -ne 0 ]; then - err "❌ Install failed: This program needs to run as root." - fi - - # Detect OS - local _os _uname _account - _uname="$(uname)" - _account="stalwart" - case "$_uname" in - Linux) _os="linux" ;; - Darwin) _os="macos"; _account="_stalwart" ;; - *) err "❌ Install failed: Unsupported OS: $_uname" ;; - esac - - # Parse arguments - local _component="stalwart" - 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 "$_prefix" ]; then - err "❌ Only one prefix argument is allowed, got: $_prefix $1" - fi - _prefix="$1" - ;; - esac - shift - done - - # 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 "$_bin_dir" "$_conf_dir" "$_log_dir" "$_data_dir" - - # Download and install the binary - say "⏳ Downloading ${_component} for ${_arch}..." - 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 - _src_name="stalwart-foundationdb" - fi - ensure cp "${_tmp}/${_src_name}" "$_bin_file" - ensure chmod 0755 "$_bin_file" - ensure rm -rf "$_tmp" - - # 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 - - # Ownership and permissions - say "🔐 Setting permissions..." - 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" - - # Install and start the service - say "🚀 Starting service..." - 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 - - # 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 -} - -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. - -# Override the hostname used in HTTP responses -#STALWART_HOSTNAME=mail.example.com - -# 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 _bin="$1" _config="$2" _env="$3" _user="$4" - cat > /etc/systemd/system/stalwart.service < /etc/init.d/stalwart < /dev/null \\ - || return 1 - start-stop-daemon --start --quiet --pidfile \$PIDFILE --exec \$DAEMON \\ - --background --make-pidfile --chuid ${_user}:${_user} \\ - -- \$DAEMON_ARGS \\ - || return 2 -} - -do_stop() -{ - 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 - start) - [ "\$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 ;; - esac - ;; - stop) - [ "\$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 ;; - esac - ;; - status) - status_of_proc "\$DAEMON" "stalwart" && exit 0 || exit \$? - ;; - restart) - log_daemon_msg "Restarting Stalwart Server" "stalwart" - do_stop - case "\$?" in - 0|1) - do_start - case "\$?" in - 0) log_end_msg 0 ;; - *) log_end_msg 1 ;; - esac - ;; - *) - log_end_msg 1 - ;; - esac - ;; - *) - echo "Usage: /etc/init.d/stalwart {start|stop|status|restart}" >&2 - exit 3 - ;; -esac - -exit 0 -EOF - chmod +x /etc/init.d/stalwart - update-rc.d stalwart defaults - service stalwart start -} - -create_service_macos() { - 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 - ServiceDescription - Stalwart - UserName - ${_user} - GroupName - ${_user} - ProgramArguments - - /bin/sh - -c - set -a; if [ -r "${_env}" ]; then . "${_env}"; fi; set +a; exec "${_bin}" --config="${_config}" - - RunAtLoad - - KeepAlive - - - -EOF - chmod 0644 "$_plist" - chown root:wheel "$_plist" - launchctl bootout system "$_plist" 2>/dev/null || true - launchctl bootstrap system "$_plist" - launchctl enable system/stalwart -} - - -get_architecture() { - local _ostype _cputype _bitness _arch _clibtype - _ostype="$(uname -s)" - _cputype="$(uname -m)" - _clibtype="gnu" - - if [ "$_ostype" = Linux ]; then - if [ "$(uname -o)" = Android ]; then - _ostype=Android - fi - if ldd --version 2>&1 | grep -q 'musl'; then - _clibtype="musl" - fi - fi - - if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then - # Darwin `uname -m` lies - if sysctl hw.optional.x86_64 | grep -q ': 1'; then - _cputype=x86_64 - fi - fi - - if [ "$_ostype" = SunOS ]; then - # Both Solaris and illumos presently announce as "SunOS" in "uname -s" - # so use "uname -o" to disambiguate. We use the full path to the - # system uname in case the user has coreutils uname first in PATH, - # which has historically sometimes printed the wrong value here. - if [ "$(/usr/bin/uname -o)" = illumos ]; then - _ostype=illumos - fi - - # illumos systems have multi-arch userlands, and "uname -m" reports the - # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86 - # systems. Check for the native (widest) instruction set on the - # running kernel: - if [ "$_cputype" = i86pc ]; then - _cputype="$(isainfo -n)" - fi - fi - - case "$_ostype" in - - Android) - _ostype=linux-android - ;; - - Linux) - check_proc - _ostype=unknown-linux-$_clibtype - _bitness=$(get_bitness) - ;; - - FreeBSD) - _ostype=unknown-freebsd - ;; - - NetBSD) - _ostype=unknown-netbsd - ;; - - DragonFly) - _ostype=unknown-dragonfly - ;; - - Darwin) - _ostype=apple-darwin - ;; - - illumos) - _ostype=unknown-illumos - ;; - - MINGW* | MSYS* | CYGWIN* | Windows_NT) - _ostype=pc-windows-gnu - ;; - - *) - err "unrecognized OS type: $_ostype" - ;; - - esac - - case "$_cputype" in - - i386 | i486 | i686 | i786 | x86) - _cputype=i686 - ;; - - xscale | arm) - _cputype=arm - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - fi - ;; - - armv6l) - _cputype=arm - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - else - _ostype="${_ostype}eabihf" - fi - ;; - - armv7l | armv8l) - _cputype=armv7 - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - else - _ostype="${_ostype}eabihf" - fi - ;; - - aarch64 | arm64) - _cputype=aarch64 - ;; - - x86_64 | x86-64 | x64 | amd64) - _cputype=x86_64 - ;; - - mips) - _cputype=$(get_endianness mips '' el) - ;; - - mips64) - if [ "$_bitness" -eq 64 ]; then - # only n64 ABI is supported for now - _ostype="${_ostype}abi64" - _cputype=$(get_endianness mips64 '' el) - fi - ;; - - ppc) - _cputype=powerpc - ;; - - ppc64) - _cputype=powerpc64 - ;; - - ppc64le) - _cputype=powerpc64le - ;; - - s390x) - _cputype=s390x - ;; - riscv64) - _cputype=riscv64gc - ;; - *) - err "unknown CPU type: $_cputype" - - esac - - # Detect 64-bit linux with 32-bit userland - if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then - case $_cputype in - x86_64) - if [ -n "${RUSTUP_CPUTYPE:-}" ]; then - _cputype="$RUSTUP_CPUTYPE" - else { - # 32-bit executable for amd64 = x32 - if is_host_amd64_elf; then { - echo "This host is running an x32 userland; as it stands, x32 support is poor," 1>&2 - echo "and there isn't a native toolchain -- you will have to install" 1>&2 - echo "multiarch compatibility with i686 and/or amd64, then select one" 1>&2 - echo "by re-running this script with the RUSTUP_CPUTYPE environment variable" 1>&2 - echo "set to i686 or x86_64, respectively." 1>&2 - echo 1>&2 - echo "You will be able to add an x32 target after installation by running" 1>&2 - echo " rustup target add x86_64-unknown-linux-gnux32" 1>&2 - exit 1 - }; else - _cputype=i686 - fi - }; fi - ;; - mips64) - _cputype=$(get_endianness mips '' el) - ;; - powerpc64) - _cputype=powerpc - ;; - aarch64) - _cputype=armv7 - if [ "$_ostype" = "linux-android" ]; then - _ostype=linux-androideabi - else - _ostype="${_ostype}eabihf" - fi - ;; - riscv64gc) - err "riscv64 with 32-bit userland unsupported" - ;; - esac - fi - - # Detect armv7 but without the CPU features Rust needs in that build, - # and fall back to arm. - # See https://github.com/rust-lang/rustup.rs/issues/587. - if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then - if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then - # At least one processor does not have NEON. - _cputype=arm - fi - fi - - _arch="${_cputype}-${_ostype}" - - RETVAL="$_arch" -} - -check_proc() { - # Check for /proc by looking for the /proc/self/exe link - # This is only run on Linux - if ! test -L /proc/self/exe ; then - err "fatal: Unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc." - fi -} - -get_bitness() { - need_cmd head - # Architecture detection without dependencies beyond coreutils. - # ELF files start out "\x7fELF", and the following byte is - # 0x01 for 32-bit and - # 0x02 for 64-bit. - # The printf builtin on some shells like dash only supports octal - # escape sequences, so we use those. - local _current_exe_head - _current_exe_head=$(head -c 5 /proc/self/exe ) - if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then - echo 32 - elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then - echo 64 - else - err "unknown platform bitness" - fi -} - -is_host_amd64_elf() { - need_cmd head - need_cmd tail - # ELF e_machine detection without dependencies beyond coreutils. - # Two-byte field at offset 0x12 indicates the CPU, - # but we're interested in it being 0x3E to indicate amd64, or not that. - local _current_exe_machine - _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1) - [ "$_current_exe_machine" = "$(printf '\076')" ] -} - -get_endianness() { - local cputype=$1 - local suffix_eb=$2 - local suffix_el=$3 - - # detect endianness without od/hexdump, like get_bitness() does. - need_cmd head - need_cmd tail - - local _current_exe_endianness - _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" - if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then - echo "${cputype}${suffix_el}" - elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then - echo "${cputype}${suffix_eb}" - else - err "unknown platform endianness" - fi -} - -say() { - printf '%s\n' "$1" -} - -err() { - say "$1" >&2 - exit 1 -} - -need_cmd() { - if ! check_cmd "$1"; then - err "need '$1' (command not found)" - fi -} - -check_cmd() { - command -v "$1" > /dev/null 2>&1 -} - -assert_nz() { - if [ -z "$1" ]; then err "assert_nz $2"; fi -} - -# Run a command that should never fail. If the command fails execution -# will immediately terminate with an error showing the failing -# command. -ensure() { - if ! "$@"; then err "command failed: $*"; fi -} - -# This wraps curl or wget. Try curl first, if not installed, -# use wget instead. -downloader() { - local _dld - local _ciphersuites - local _err - local _status - local _retry - if check_cmd curl; then - _dld=curl - elif check_cmd wget; then - _dld=wget - else - _dld='curl or wget' # to be used in error message of need_cmd - fi - - if [ "$1" = --check ]; then - need_cmd "$_dld" - elif [ "$_dld" = curl ]; then - check_curl_for_retry_support - _retry="$RETVAL" - get_ciphersuites_for_curl - _ciphersuites="$RETVAL" - if [ -n "$_ciphersuites" ]; then - _err=$(curl $_retry --proto '=https' --tlsv1.2 --ciphers "$_ciphersuites" --silent --show-error --fail --location "$1" --output "$2" 2>&1) - _status=$? - else - echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" - if ! check_help_for "$3" curl --proto --tlsv1.2; then - echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" - _err=$(curl $_retry --silent --show-error --fail --location "$1" --output "$2" 2>&1) - _status=$? - else - _err=$(curl $_retry --proto '=https' --tlsv1.2 --silent --show-error --fail --location "$1" --output "$2" 2>&1) - _status=$? - fi - fi - if [ -n "$_err" ]; then - if echo "$_err" | grep -q 404; then - err "❌ Binary for platform '$3' not found, this platform may be unsupported." - else - echo "$_err" >&2 - fi - fi - return $_status - elif [ "$_dld" = wget ]; then - if [ "$(wget -V 2>&1|head -2|tail -1|cut -f1 -d" ")" = "BusyBox" ]; then - echo "Warning: using the BusyBox version of wget. Not enforcing strong cipher suites for TLS or TLS v1.2, this is potentially less secure" - _err=$(wget "$1" -O "$2" 2>&1) - _status=$? - else - get_ciphersuites_for_wget - _ciphersuites="$RETVAL" - if [ -n "$_ciphersuites" ]; then - _err=$(wget --https-only --secure-protocol=TLSv1_2 --ciphers "$_ciphersuites" "$1" -O "$2" 2>&1) - _status=$? - else - echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" - if ! check_help_for "$3" wget --https-only --secure-protocol; then - echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" - _err=$(wget "$1" -O "$2" 2>&1) - _status=$? - else - _err=$(wget --https-only --secure-protocol=TLSv1_2 "$1" -O "$2" 2>&1) - _status=$? - fi - fi - fi - if [ -n "$_err" ]; then - if echo "$_err" | grep -q ' 404 Not Found'; then - err "❌ Binary for platform '$3' not found, this platform may be unsupported." - else - echo "$_err" >&2 - fi - fi - return $_status - else - err "Unknown downloader" # should not reach here - fi -} - -# Check if curl supports the --retry flag, then pass it to the curl invocation. -check_curl_for_retry_support() { - local _retry_supported="" - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "curl" "--retry"; then - _retry_supported="--retry 3" - fi - - RETVAL="$_retry_supported" - -} - -check_help_for() { - local _arch - local _cmd - local _arg - _arch="$1" - shift - _cmd="$1" - shift - - local _category - if "$_cmd" --help | grep -q 'For all options use the manual or "--help all".'; then - _category="all" - else - _category="" - fi - - case "$_arch" in - - *darwin*) - if check_cmd sw_vers; then - case $(sw_vers -productVersion) in - 10.*) - # If we're running on macOS, older than 10.13, then we always - # fail to find these options to force fallback - if [ "$(sw_vers -productVersion | cut -d. -f2)" -lt 13 ]; then - # Older than 10.13 - echo "Warning: Detected macOS platform older than 10.13" - return 1 - fi - ;; - 11.*) - # We assume Big Sur will be OK for now - ;; - *) - # Unknown product version, warn and continue - echo "Warning: Detected unknown macOS major version: $(sw_vers -productVersion)" - echo "Warning TLS capabilities detection may fail" - ;; - esac - fi - ;; - - esac - - for _arg in "$@"; do - if ! "$_cmd" --help $_category | grep -q -- "$_arg"; then - return 1 - fi - done - - true # not strictly needed -} - -# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites -# if support by local tools is detected. Detection currently supports these curl backends: -# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. -get_ciphersuites_for_curl() { - if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then - # user specified custom cipher suites, assume they know what they're doing - RETVAL="$RUSTUP_TLS_CIPHERSUITES" - return - fi - - local _openssl_syntax="no" - local _gnutls_syntax="no" - local _backend_supported="yes" - if curl -V | grep -q ' OpenSSL/'; then - _openssl_syntax="yes" - elif curl -V | grep -iq ' LibreSSL/'; then - _openssl_syntax="yes" - elif curl -V | grep -iq ' BoringSSL/'; then - _openssl_syntax="yes" - elif curl -V | grep -iq ' GnuTLS/'; then - _gnutls_syntax="yes" - else - _backend_supported="no" - fi - - local _args_supported="no" - if [ "$_backend_supported" = "yes" ]; then - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "curl" "--tlsv1.2" "--ciphers" "--proto"; then - _args_supported="yes" - fi - fi - - local _cs="" - if [ "$_args_supported" = "yes" ]; then - if [ "$_openssl_syntax" = "yes" ]; then - _cs=$(get_strong_ciphersuites_for "openssl") - elif [ "$_gnutls_syntax" = "yes" ]; then - _cs=$(get_strong_ciphersuites_for "gnutls") - fi - fi - - RETVAL="$_cs" -} - -# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites -# if support by local tools is detected. Detection currently supports these wget backends: -# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. -get_ciphersuites_for_wget() { - if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then - # user specified custom cipher suites, assume they know what they're doing - RETVAL="$RUSTUP_TLS_CIPHERSUITES" - return - fi - - local _cs="" - if wget -V | grep -q '\-DHAVE_LIBSSL'; then - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then - _cs=$(get_strong_ciphersuites_for "openssl") - fi - elif wget -V | grep -q '\-DHAVE_LIBGNUTLS'; then - # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. - if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then - _cs=$(get_strong_ciphersuites_for "gnutls") - fi - fi - - RETVAL="$_cs" -} - -# Return strong TLS 1.2-1.3 cipher suites in OpenSSL or GnuTLS syntax. TLS 1.2 -# excludes non-ECDHE and non-AEAD cipher suites. DHE is excluded due to bad -# DH params often found on servers (see RFC 7919). Sequence matches or is -# similar to Firefox 68 ESR with weak cipher suites disabled via about:config. -# $1 must be openssl or gnutls. -get_strong_ciphersuites_for() { - if [ "$1" = "openssl" ]; then - # OpenSSL is forgiving of unknown values, no problems with TLS 1.3 values on versions that don't support it yet. - echo "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384" - elif [ "$1" = "gnutls" ]; then - # GnuTLS isn't forgiving of unknown values, so this may require a GnuTLS version that supports TLS 1.3 even if wget doesn't. - # Begin with SECURE128 (and higher) then remove/add to build cipher suites. Produces same 9 cipher suites as OpenSSL but in slightly different order. - echo "SECURE128:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS-ALL:-CIPHER-ALL:-MAC-ALL:-KX-ALL:+AEAD:+ECDHE-ECDSA:+ECDHE-RSA:+AES-128-GCM:+CHACHA20-POLY1305:+AES-256-GCM" - fi -} - -# This is just for indicating that commands' results are being -# intentionally ignored. Usually, because it's being executed -# as part of error handling. -ignore() { - "$@" -} - -main "$@" || exit 1 diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index a6ac7ab5..79e03cf3 100644 Binary files a/resources/schema/schema.json.gz and b/resources/schema/schema.json.gz differ diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 8335e5e9..d0d93ad4 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -_NV9JZvmSHwGCNQ7Gw4Uj027Bpp2UT3LGf2zHyp4tOM \ No newline at end of file +aJJKvnpsjjwAEzJ0eKDWdfSLK_RvBKsLhk8BwCq-7qA \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 644a8a36..b80ff36f 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [features] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "nats", "azure", "foundationdb"] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"] -default = ["rocks", "sqlite", "redis"] +default = ["rocks", "foundationdb"] sqlite = ["store/sqlite", "directory/sqlite"] foundationdb = ["store/foundation", "common/foundation"] postgres = ["store/postgres", "directory/postgres"] diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index e57fcf4d..71e34181 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -50,8 +50,7 @@ services: # FoundationDB # --------------------------------------------------------------------------- foundationdb: - image: foundationdb/foundationdb:7.3.43 - platform: linux/amd64 + image: foundationdb/foundationdb:7.4.6 ports: - "127.0.0.1:4500:4500" environment: @@ -63,8 +62,7 @@ services: - /var/fdb/logs fdb-init: - image: foundationdb/foundationdb:7.3.43 - platform: linux/amd64 + image: foundationdb/foundationdb:7.4.6 depends_on: - foundationdb volumes: diff --git a/tests/src/automation/dns.rs b/tests/src/automation/dns.rs index a72e9882..5e60b746 100644 --- a/tests/src/automation/dns.rs +++ b/tests/src/automation/dns.rs @@ -58,7 +58,7 @@ _995._tcp.pop3.example.org. IN TLSA 2 1 1 _dmarc.example.org. IN TXT "v=DMARC1; p=reject; rua=mailto:postmaster@example.org" _mta-sts.example.org. IN TXT "v=STSv1; id=12942536112359691423" _smtp._tls.example.org. IN TXT "v=TLSRPTv1; rua=mailto:postmaster@example.org" -_ua-auto-config.example.org. IN TXT "v=UAAC1; a=sha256; d=9ZXAG5NGLnhdx4IQKuA2lLGrVXD3N/UKlo059TEi9zY=" +_ua-auto-config.example.org. IN TXT "v=UAAC1; a=sha256; d=JJ/h5yIbCwPiASVbENZzCyyzeV/AiFCoBbiH2iTISWs=" _validation-persist.example.org. IN TXT "pebble.letsencrypt.org; accounturi=REDACTED" dummy-v1-ed25519._domainkey.example.org. IN TXT "v=DKIM1; k=ed25519; h=sha256; p=REDACTED" dummy-v1-rsa._domainkey.example.org. IN TXT "v=DKIM1; k=rsa; h=sha256; p=REDACTED" diff --git a/tests/src/directory/integration.rs b/tests/src/directory/integration.rs index e1dd11d9..dfde1edd 100644 --- a/tests/src/directory/integration.rs +++ b/tests/src/directory/integration.rs @@ -14,6 +14,7 @@ use registry::schema::structs::{Account, AccountSettings, Directory}; use types::id::Id; pub async fn test() { + println!("Running directory integration tests..."); let test = TestServerBuilder::new("directory_integration_test") .await .with_default_listeners() diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 64bfe693..498c0fb0 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -11,6 +11,7 @@ use registry::{ }; pub async fn test() { + println!("Running LDAP directory tests..."); let mut config = ldap_test_directory(); // Test bind authentication @@ -156,6 +157,7 @@ pub fn ldap_test_directory() -> structs::LdapDirectory { .into(), group_class: "groupOfNames".into(), bind_authentication: true, + description: "Test LDAP directory".into(), ..Default::default() } } diff --git a/tests/src/directory/oidc.rs b/tests/src/directory/oidc.rs index 6c5b3883..48d2a5e9 100644 --- a/tests/src/directory/oidc.rs +++ b/tests/src/directory/oidc.rs @@ -12,6 +12,7 @@ use directory::{Account, Credentials, Directory, backend::oidc::OpenIdDirectory} use registry::{schema::structs, types::map::Map}; pub async fn test() { + println!("Running OIDC directory tests..."); let config = structs::OidcDirectory { description: "Test OIDC directory".to_string(), issuer_url: "http://localhost:9080/realms/stalwart".to_string(), diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index ac801b14..f909974d 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -9,6 +9,7 @@ use registry::schema::structs::{self, SqlAuthStore}; use store::{Store, backend::sqlite::SqliteStore}; pub async fn test() { + println!("Running SQL directory tests..."); let sql_store = Store::SQLite(SqliteStore::open_memory().unwrap().into()); // Create test directory diff --git a/tests/src/directory/synchronization.rs b/tests/src/directory/synchronization.rs index 71987375..1413f512 100644 --- a/tests/src/directory/synchronization.rs +++ b/tests/src/directory/synchronization.rs @@ -12,6 +12,7 @@ use registry::schema::{ use types::id::Id; pub async fn test() { + println!("Running directory synchronization tests..."); let test = TestServerBuilder::new("directory_synchronization_test") .await .with_default_listeners()