Packaging: the binary and package are inbuxa, INBUXA_* settings with STALWART_* fallback

- crates/main: package and [[bin]] renamed to inbuxa; homepage inbuxa.org;
  license AGPL-3.0-only (upstream is dual; the fork takes the AGPL).
- types::branding::env_var reads INBUXA_<name>, falling back to
  STALWART_<name> with a warning, for all nine server settings.
  STALWART_APP_ and STALWART_SPAM_* storage keys are unchanged.
- New-install default paths /var/lib/inbuxa and /var/log/inbuxa.
- Dockerfiles, systemd unit, launchd plist and AppArmor profile renamed.
- Upstream's .github moved to .github-upstream so none of it runs.
- install.sh stubbed: upstream's would install Stalwart.
- Two missed brand strings: the SMTP Received header and the utils user agent.
This commit is contained in:
2026-09-18 11:09:22 -07:00
parent 5ce033e10c
commit d3f0b36dd2
35 changed files with 199 additions and 1205 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ impl Listeners {
bind: Map::new(vec![
SocketAddr::from_str(&format!(
"[::]:{}",
std::env::var("STALWART_RECOVERY_MODE_PORT")
types::branding::env_var("RECOVERY_MODE_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(8080)
+1 -1
View File
@@ -503,7 +503,7 @@ impl Tracers {
}
} else {
// Add default tracer if none were found
let level = std::env::var("STALWART_RECOVERY_MODE_LOG_LEVEL")
let level = types::branding::env_var("RECOVERY_MODE_LOG_LEVEL")
.ok()
.and_then(|level| Level::from_str(&level).ok())
.unwrap_or(Level::Info);
+1 -1
View File
@@ -43,7 +43,7 @@ const HELP: &str = concat!(
env!("CARGO_PKG_VERSION"),
r#"
Usage: stalwart [OPTIONS]
Usage: inbuxa [OPTIONS]
Options:
-c, --config <PATH> Start server with the specified configuration file
+1 -1
View File
@@ -529,7 +529,7 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> {
ansi: false,
prefix: "stalwart.log".into(),
rotate: LogRotateFrequency::Daily,
path: "/var/log/stalwart".into(),
path: "/var/log/inbuxa".into(),
..Default::default()
})
.into(),
@@ -657,7 +657,7 @@ fn map_dns_server(dns_server: &DnsServerBootstrap) -> Option<registry::schema::s
const DEFAULT_DATA_PATH: &str = if cfg!(target_os = "freebsd") {
"/var/db/stalwart/"
} else {
"/var/lib/stalwart/"
"/var/lib/inbuxa/"
};
fn build_default_bootstrap(server: &Server) -> Bootstrap {
@@ -676,7 +676,7 @@ fn build_default_bootstrap(server: &Server) -> Bootstrap {
in_memory_store: InMemoryStore::Default,
directory: DirectoryBootstrap::Internal,
tracer: Tracer::Log(TracerLog {
path: "/var/log/stalwart/".to_string(),
path: "/var/log/inbuxa/".to_string(),
prefix: "stalwart".to_string(),
ansi: true,
enable: true,
+5 -5
View File
@@ -1,17 +1,17 @@
[package]
name = "stalwart"
name = "inbuxa"
description = "INBUXA Mail and Collaboration Server, a fork of Stalwart"
authors = [ "Stalwart Labs LLC <[email protected]>"]
repository = "https://github.com/stalwartlabs/stalwart"
homepage = "https://stalw.art"
homepage = "https://inbuxa.org"
keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"]
categories = ["email"]
license = "AGPL-3.0-only OR LicenseRef-SEL"
# Upstream offers AGPL-3.0-only OR LicenseRef-SEL; INBUXA takes the AGPL only.
license = "AGPL-3.0-only"
version = "0.16.22"
edition = "2024"
[[bin]]
name = "stalwart"
name = "inbuxa"
path = "src/main.rs"
[dependencies]
+2 -2
View File
@@ -4143,7 +4143,7 @@ impl Default for Bootstrap {
request_tls_certificate: true,
generate_dkim_keys: true,
data_store: DataStore::RocksDb(RocksDbStore {
path: "/var/lib/stalwart/".to_string(),
path: "/var/lib/inbuxa/".to_string(),
..Default::default()
}),
blob_store: BlobStore::Default,
@@ -4151,7 +4151,7 @@ impl Default for Bootstrap {
in_memory_store: InMemoryStore::Default,
directory: DirectoryBootstrap::Internal,
tracer: Tracer::Log(TracerLog {
path: "/var/log/stalwart/".to_string(),
path: "/var/log/inbuxa/".to_string(),
..Default::default()
}),
dns_server: DnsServerBootstrap::Manual,
+1 -1
View File
@@ -1051,7 +1051,7 @@ impl<T: SessionStream> Session<T> {
}
headers.extend_from_slice(b"by ");
headers.extend_from_slice(self.hostname.as_bytes());
headers.extend_from_slice(b" (Stalwart SMTP) with ");
headers.extend_from_slice(concat!(" (", types::brand!(), " SMTP) with ").as_bytes());
headers.extend_from_slice(match (self.stream.is_tls(), !self.is_authenticated()) {
(true, true) => b"ESMTPS",
(true, false) => b"ESMTPSA",
+3 -3
View File
@@ -31,18 +31,18 @@ impl RegistryStore {
.collect::<String>();
eprintln!();
eprintln!("════════════════════════════════════════════════════════════");
eprintln!("🔑 Stalwart bootstrap mode - temporary administrator account");
eprintln!("🔑 INBUXA 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!("/admin web UI. Once setup is done, the server 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!("instead, set INBUXA_RECOVERY_ADMIN=admin:<password> in the");
eprintln!("env file.");
eprintln!("════════════════════════════════════════════════════════════");
eprintln!();
+7 -7
View File
@@ -17,7 +17,7 @@ pub(crate) enum RegistryInit {
impl RegistryStoreInner {
pub(crate) fn new(local_path: PathBuf) -> Self {
let env_hostname = std::env::var("STALWART_HOSTNAME")
let env_hostname = types::branding::env_var("HOSTNAME")
.ok()
.filter(|h| !h.is_empty())
.unwrap_or_else(|| {
@@ -35,30 +35,30 @@ impl RegistryStoreInner {
store: Store::None,
id_generator: SnowflakeIdGenerator::new(),
node_id: 0,
env_recovery_mode: std::env::var("STALWART_RECOVERY_MODE")
env_recovery_mode: types::branding::env_var("RECOVERY_MODE")
.ok()
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false),
env_recovery_admin: std::env::var("STALWART_RECOVERY_ADMIN")
env_recovery_admin: types::branding::env_var("RECOVERY_ADMIN")
.ok()
.and_then(|v| {
v.split_once(':')
.map(|(a, p)| (a.trim().to_string(), p.trim().to_string()))
})
.filter(|(a, p)| !a.is_empty() && !p.is_empty()),
env_cluster_role: std::env::var("STALWART_ROLE")
env_cluster_role: types::branding::env_var("ROLE")
.ok()
.filter(|r| !r.is_empty()),
env_push_shard_id: std::env::var("STALWART_PUSH_SHARD")
env_push_shard_id: types::branding::env_var("PUSH_SHARD")
.ok()
.and_then(|id| id.parse::<u32>().ok().and_then(|v| v.checked_sub(1)))
.unwrap_or(0),
env_public_url: std::env::var("STALWART_PUBLIC_URL")
env_public_url: types::branding::env_var("PUBLIC_URL")
.ok()
.map(|v| v.trim().trim_end_matches('/').to_string())
.filter(|u| !u.is_empty())
.or_else(|| {
std::env::var("STALWART_HTTPS_PORT").ok().and_then(|p| {
types::branding::env_var("HTTPS_PORT").ok().and_then(|p| {
p.parse::<u16>()
.ok()
.map(|port| format!("https://{}:{}", env_hostname, port))
+19
View File
@@ -45,3 +45,22 @@ macro_rules! brand_url {
"https://inbuxa.org"
};
}
/// Reads one of the server's environment variables by its unprefixed name,
/// such as `RECOVERY_ADMIN`.
///
/// `INBUXA_<name>` wins. `STALWART_<name>` is still read when the new name
/// isn't set, so an existing Stalwart install moves over without editing its
/// environment, and a warning says which variable to rename.
pub fn env_var(name: &str) -> Result<String, std::env::VarError> {
match std::env::var(format!("INBUXA_{name}")) {
Err(std::env::VarError::NotPresent) => {
let legacy = std::env::var(format!("STALWART_{name}"));
if legacy.is_ok() {
eprintln!("Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead.");
}
legacy
}
found => found,
}
}
+1 -1
View File
@@ -145,7 +145,7 @@ pub fn build_http_client(
allow_invalid_certs: bool,
) -> Result<Client, String> {
let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?;
headers.insert(USER_AGENT, "Stalwart/1.0.0".parse().unwrap());
headers.insert(USER_AGENT, "INBUXA/1.0.0".parse().unwrap()); // types::brand!(); utils does not depend on types
match http_client_builder(allow_invalid_certs)
.connect_timeout(timeout)