diff --git a/crates/common/src/config/telemetry.rs b/crates/common/src/config/telemetry.rs index d3fa7aa..8cedbcd 100644 --- a/crates/common/src/config/telemetry.rs +++ b/crates/common/src/config/telemetry.rs @@ -543,10 +543,10 @@ impl Metrics { let metrics = bp.setting_infallible::().await; let resource = Resource::builder() .with_service_name("stalwart") - .with_attribute(KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))) + .with_attribute(KeyValue::new(SERVICE_VERSION, types::brand_version_full!())) .build(); let instrumentation = InstrumentationScope::builder("stalwart") - .with_version(env!("CARGO_PKG_VERSION")) + .with_version(types::brand_version_full!()) .build(); Metrics { diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a29b28c..f6061ce 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -79,7 +79,7 @@ pub static VERSION_PRIVATE: &str = env!("CARGO_PKG_VERSION"); pub static VERSION_PUBLIC: &str = "1.0.0"; pub static USER_AGENT: &str = concat!(types::brand!(), "/1.0.0"); -pub static DAEMON_NAME: &str = concat!(types::brand!(), " v", env!("CARGO_PKG_VERSION"),); +pub static DAEMON_NAME: &str = concat!(types::brand!(), " v", types::brand_version!(),); pub static PROD_ID: &str = types::brand_prodid!(); /* diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index 1d99026..5f1f5de 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -39,8 +39,8 @@ pub struct IpcReceivers { const HELP: &str = concat!( types::brand_server!(), - " v", - env!("CARGO_PKG_VERSION"), + " ", + types::brand_version_full!(), r#" Usage: inbuxa [OPTIONS] @@ -88,7 +88,7 @@ impl BootManager { std::process::exit(0); } ("version" | "V", _) => { - println!("{}", env!("CARGO_PKG_VERSION")); + println!("{}", types::brand_version_full!()); std::process::exit(0); } ("config" | "c", Some(value)) => { @@ -166,20 +166,20 @@ impl BootManager { Hostname = bootstrap.registry.local_hostname().to_string(), Details = "No configuration file was found. Port 8080 is open for initial setup.", - Version = env!("CARGO_PKG_VERSION"), + Version = types::brand_version_full!(), ); } else if bootstrap.registry.is_recovery_mode() { trc::event!( Server(trc::ServerEvent::RecoveryMode), Details = "Port 8080 is open for troubleshooting and recovery.", Hostname = bootstrap.registry.local_hostname().to_string(), - Version = env!("CARGO_PKG_VERSION"), + Version = types::brand_version_full!(), ); } else { trc::event!( Server(trc::ServerEvent::Startup), Hostname = bootstrap.registry.local_hostname().to_string(), - Version = env!("CARGO_PKG_VERSION"), + Version = types::brand_version_full!(), ); } diff --git a/crates/common/src/manager/console.rs b/crates/common/src/manager/console.rs index 4dd1fd8..0058a3b 100644 --- a/crates/common/src/manager/console.rs +++ b/crates/common/src/manager/console.rs @@ -13,8 +13,8 @@ use store::{Deserialize, IterateParams, SUBSPACE_INDEXES, SUBSPACE_REGISTRY_IDX, const HELP: &str = concat!( types::brand_server!(), - " v", - env!("CARGO_PKG_VERSION"), + " ", + types::brand_version_full!(), r#" Data Store CLI Enter commands (type 'help' for available commands). diff --git a/crates/common/src/telemetry/tracers/otel.rs b/crates/common/src/telemetry/tracers/otel.rs index 79903c8..f84359b 100644 --- a/crates/common/src/telemetry/tracers/otel.rs +++ b/crates/common/src/telemetry/tracers/otel.rs @@ -28,11 +28,11 @@ pub(crate) fn spawn_otel_tracer(builder: SubscriberBuilder, mut otel: OtelTracer tokio::spawn(async move { let resource = Resource::builder() .with_service_name("stalwart") - .with_attribute(KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION"))) + .with_attribute(KeyValue::new(SERVICE_VERSION, types::brand_version_full!())) .build(); let instrumentation = InstrumentationScope::builder("stalwart") - .with_version(env!("CARGO_PKG_VERSION")) + .with_version(types::brand_version_full!()) .build(); otel.log_exporter.set_resource(&resource); diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index e1ac102..de79b50 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -70,7 +70,9 @@ impl Session { concat!( "* ID (\"name\" \"", types::brand!(), - "\" \"version\" \"1.0.0\" \"vendor\" \"", + "\" \"version\" \"", + types::brand_version!(), + "\" \"vendor\" \"", types::brand!(), "\" \"support-url\" \"", types::brand_url!(), diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index 39a9f8d..22124a6 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -407,7 +407,7 @@ impl Session { impl Default for SieveSessionCapabilities { fn default() -> Self { Self { - implementation: concat!(types::brand!(), " v1.0.0"), + implementation: concat!(types::brand!(), " ", types::brand_version_full!()), } } } diff --git a/crates/types/src/branding.rs b/crates/types/src/branding.rs index ecd896f..427d578 100644 --- a/crates/types/src/branding.rs +++ b/crates/types/src/branding.rs @@ -64,3 +64,25 @@ pub fn env_var(name: &str) -> Result { found => found, } } + +/// INBUXA's own version, dated like the rest of its family: `YYYY.M.D`, with +/// a letter or `.N` suffix for a second release on one day. It's set here and +/// not in Cargo.toml, so upstream's version bumps merge without conflicts. +#[macro_export] +macro_rules! brand_version { + () => { + "2026.9.18" + }; +} + +/// The version with the Stalwart release it's built on, e.g. +/// `2026.9.18 (Stalwart 0.16.22)`. The base comes from Cargo, which follows +/// upstream, so it's always the base actually compiled in. It matters because +/// Stalwart's data upgrades are one-way. Once INBUXA stops tracking upstream, +/// this becomes just the version. +#[macro_export] +macro_rules! brand_version_full { + () => { + concat!($crate::brand_version!(), " (Stalwart ", env!("CARGO_PKG_VERSION"), ")") + }; +} diff --git a/docs/spec/SPEC.md b/docs/spec/SPEC.md index ae0ccec..1a6b5f4 100644 --- a/docs/spec/SPEC.md +++ b/docs/spec/SPEC.md @@ -209,6 +209,36 @@ Done 2026-09-18: - `install.sh` is a stub that says there's no release yet. Upstream's version would download and install Stalwart itself. +### 2.6 Versioning + +Decided 2026-09-18. + +- **INBUXA has its own version,** dated like the rest of its family + (ihasmail, ihasmail-oneshot, stalwart-migrator): `YYYY.M.D`, tagged + `vYYYY.M.D`, with a suffix for a second release on one day. The server's is + `types::brand_version!()`, and INBUXA Admin has its own. +- **The Stalwart base stays visible** while INBUXA tracks upstream: + `inbuxa --version`, the startup banner and events, OpenTelemetry's + `service.version`, and the JMAP `implementation` string all read + `2026.9.18 (Stalwart 0.16.22)`. It matters because Stalwart's data upgrades + are one-way, so anyone upgrading needs to know which base their data will be + converted to. Release notes and the strip report say it too. +- **Cargo versions follow upstream, untouched.** Every upstream release bumps + all ~30 manifests, so overriding them would conflict on every sync. The base + shown is read from Cargo, so it's always what was actually compiled in. + Upstream's internal data-format version (`types/src/semver.rs`), which drives + store migrations, is upstream's and is never changed. +- `DAEMON_NAME` (the `Received:` header) and the IMAP `ID` response carry the + INBUXA version alone. `VERSION_PUBLIC`'s deliberately vague `1.0.0` for Sieve + is left as upstream has it. +- **Stalwart 1.0** arrives like any release (strip, merge, release under the + next date), but as its own milestone: a merge dry run, a strip-report + comparison against 0.16, and an upgrade test on a copy of real data, because + it will almost certainly bring a one-way store migration. +- **Diverging from upstream** is expected eventually, though not soon. When it + happens, the base drops out of the version string and nothing else about + versioning changes. + ## 3. Clean room INBUXA runs on a paid Stalwart Enterprise license, so its maintainer is a diff --git a/tests/src/jmap/principal/get.rs b/tests/src/jmap/principal/get.rs index 3be7f5d..0c786f6 100644 --- a/tests/src/jmap/principal/get.rs +++ b/tests/src/jmap/principal/get.rs @@ -54,7 +54,7 @@ pub async fn test(test: &TestServer) { "urn:ietf:params:jmap:submission": {}, "urn:ietf:params:jmap:vacationresponse": {}, "urn:ietf:params:jmap:sieve": { - "implementation": "INBUXA v1.0.0" + "implementation": concat!(types::brand!(), " ", types::brand_version_full!()) }, "urn:ietf:params:jmap:blob": {}, "urn:ietf:params:jmap:quota": {},