Quick fixes from the first boot: recovery admin, upsell, warnings

- The recovery administrator (INBUXA_RECOVERY_ADMIN, or STALWART_RECOVERY_ADMIN)
  is honored only in bootstrap and recovery mode. On a configured server it's
  ignored with a startup warning. Before, it was a standing full-admin login
  for as long as the variable stayed set.
- The Enterprise upsell error is replaced by "This feature isn't available in
  INBUXA yet" for the features still to be rebuilt.
- Workspace warnings: 25 to 0. cargo fix removed the unused imports. The
  seven places where Enterprise code used to plug in keep their parameters,
  each with an inbuxa: comment naming the rebuild that uses it again. The
  antispam test's mock-server imports are back behind pending-rebuild.
This commit is contained in:
2026-09-18 11:36:44 -07:00
parent d3f0b36dd2
commit c9c761fab5
16 changed files with 51 additions and 30 deletions
+4 -2
View File
@@ -514,13 +514,15 @@ impl Server {
pub async fn get_directory_for_domain( pub async fn get_directory_for_domain(
&self, &self,
domain_name: &str, // inbuxa: unused until per-domain directories (Domain.directoryId) are rebuilt; see docs/spec/SPEC.md §4
_domain_name: &str,
) -> trc::Result<Option<&Arc<Directory>>> { ) -> trc::Result<Option<&Arc<Directory>>> {
Ok(self.get_default_directory()) Ok(self.get_default_directory())
} }
pub fn get_directory_for_cached_domain(&self, domain: &DomainCache) -> Option<&Arc<Directory>> { // inbuxa: `_domain` is unused until per-domain directories (Domain.directoryId) are rebuilt
pub fn get_directory_for_cached_domain(&self, _domain: &DomainCache) -> Option<&Arc<Directory>> {
self.get_default_directory() self.get_default_directory()
} }
+2 -1
View File
@@ -45,7 +45,8 @@ impl Server {
&self, &self,
permissions: &structs::Permissions, permissions: &structs::Permissions,
role_ids: &[Id], role_ids: &[Id],
tenant_id: Option<u32>, // inbuxa: unused until multi-tenancy is rebuilt: the tenant permission ceiling (docs/spec/features/multi-tenancy.md MT-13)
_tenant_id: Option<u32>,
) -> trc::Result<PermissionsGroup> { ) -> trc::Result<PermissionsGroup> {
// Calculate effective permissions // Calculate effective permissions
let (mut permissions, roles) = match permissions { let (mut permissions, roles) = match permissions {
-1
View File
@@ -17,7 +17,6 @@ use registry::{
}, },
types::id::ObjectId, types::id::ObjectId,
}; };
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
use types::id::Id; use types::id::Id;
#[derive(Debug, Default)] #[derive(Debug, Default)]
+2 -2
View File
@@ -28,7 +28,7 @@ use registry::{
enums::{DkimRotationStage, Locale, StorageQuota, TenantStorageQuota}, enums::{DkimRotationStage, Locale, StorageQuota, TenantStorageQuota},
prelude::{ObjectType, Property}, prelude::{ObjectType, Property},
structs::{ structs::{
Account, DkimSignature, Domain, EncryptionAtRest, MailingList, MaskedEmail, Account, DkimSignature, Domain, EncryptionAtRest, MailingList,
Permissions, PublicKey, Role, SubAddressing, Tenant, Permissions, PublicKey, Role, SubAddressing, Tenant,
}, },
}, },
@@ -38,7 +38,7 @@ use std::{borrow::Cow, sync::Arc};
use store::{ use store::{
U64_LEN, U64_LEN,
registry::{RegistryQuery, bootstrap::Bootstrap}, registry::{RegistryQuery, bootstrap::Bootstrap},
write::{key::KeySerializer, now}, write::key::KeySerializer,
}; };
use trc::{AddContext, StoreEvent}; use trc::{AddContext, StoreEvent};
use types::id::Id; use types::id::Id;
+1 -1
View File
@@ -38,7 +38,7 @@ pub mod storage;
pub mod telemetry; pub mod telemetry;
impl Core { impl Core {
pub async fn parse(bp: &mut Bootstrap, mut storage: Storage) -> Self { pub async fn parse(bp: &mut Bootstrap, storage: Storage) -> Self {
Self { Self {
sieve: Scripting::parse(bp).await, sieve: Scripting::parse(bp).await,
+2 -1
View File
@@ -141,7 +141,8 @@ impl Telemetry {
} }
impl Tracers { impl Tracers {
pub async fn parse(bp: &mut Bootstrap, storage: &Storage) -> Self { // inbuxa: `_storage` is unused until monitoring history (stored traces and metrics) is rebuilt
pub async fn parse(bp: &mut Bootstrap, _storage: &Storage) -> Self {
let mut custom_levels = AHashMap::new(); let mut custom_levels = AHashMap::new();
let mut tracers: Vec<TelemetrySubscriber> = Vec::new(); let mut tracers: Vec<TelemetrySubscriber> = Vec::new();
let mut global_interests = Interests::default(); let mut global_interests = Interests::default();
+3
View File
@@ -161,6 +161,9 @@ pub struct Data {
pub struct LogoCache { pub struct LogoCache {
domain_id: u32, domain_id: u32,
tenant_id: Option<u32>, tenant_id: Option<u32>,
// inbuxa: read again when the /logo endpoint (per-tenant and per-domain
// branding) is rebuilt; docs/spec/features/multi-tenancy.md MT-22.
#[allow(dead_code)]
data: Option<Resource<Vec<u8>>>, data: Option<Resource<Vec<u8>>>,
} }
+2 -3
View File
@@ -23,7 +23,7 @@ use crate::{
}; };
use directory::Recipient; use directory::Recipient;
use mail_auth::IpLookupStrategy; use mail_auth::IpLookupStrategy;
use registry::schema::{enums::ExpressionVariable, structs::MaskedEmail}; use registry::schema::enums::ExpressionVariable;
use sieve::Sieve; use sieve::Sieve;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@@ -32,10 +32,9 @@ use std::{
}; };
use store::{ use store::{
Deserialize, IterateParams, ValueKey, Deserialize, IterateParams, ValueKey,
write::{AlignedBytes, Archive, QueueClass, ValueClass, now}, write::{AlignedBytes, Archive, QueueClass, ValueClass},
}; };
use trc::{AddContext, SpamEvent}; use trc::{AddContext, SpamEvent};
use types::id::Id;
impl Server { impl Server {
pub async fn rcpt_resolve( pub async fn rcpt_resolve(
@@ -4,9 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
use sieve::{FunctionMap, compiler::Number, runtime::Variable}; use sieve::{FunctionMap, runtime::Variable};
use std::time::Instant;
use trc::{AiEvent, SecurityEvent};
use super::PluginContext; use super::PluginContext;
@@ -14,7 +12,8 @@ pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) {
fnc_map.set_external_function("llm_prompt", plugin_id, 3); fnc_map.set_external_function("llm_prompt", plugin_id, 3);
} }
pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { // inbuxa: the LLM Sieve function is a no-op until AI classification is rebuilt
pub async fn exec(_ctx: PluginContext<'_>) -> trc::Result<Variable> {
Ok(false.into()) Ok(false.into())
} }
+2 -1
View File
@@ -96,7 +96,8 @@ impl Telemetry {
} }
impl TelemetrySubscriberType { impl TelemetrySubscriberType {
pub fn spawn(self, builder: SubscriberBuilder, is_enterprise: bool) { // inbuxa: `_is_enterprise` is unused until monitoring history is rebuilt, and goes when it is: there is one edition
pub fn spawn(self, builder: SubscriberBuilder, _is_enterprise: bool) {
match self { match self {
TelemetrySubscriberType::ConsoleTracer(settings) => { TelemetrySubscriberType::ConsoleTracer(settings) => {
spawn_console_tracer(builder, settings) spawn_console_tracer(builder, settings)
@@ -20,10 +20,7 @@ use registry::{
}, },
types::EnumImpl, types::EnumImpl,
}; };
use store::{ use store::write::{BatchBuilder, RegistryClass, ValueClass, now};
registry::{RegistryObjectCounter, RegistryQuery},
write::{BatchBuilder, RegistryClass, ValueClass, now},
};
use trc::AddContext; use trc::AddContext;
use types::id::Id; use types::id::Id;
@@ -22,7 +22,7 @@ use registry::{
jmap::{IntoValue, JsonPointerPatch, RegistryJsonPatch}, jmap::{IntoValue, JsonPointerPatch, RegistryJsonPatch},
schema::{ schema::{
enums::{DeliveryErrorType, MessageFlag, RecipientFlag}, enums::{DeliveryErrorType, MessageFlag, RecipientFlag},
prelude::{ObjectType, Property}, prelude::Property,
structs::{ structs::{
DeliveryError, QueueExpiry, QueueExpiryAttempts, QueueExpiryTtl, QueuedMessage, DeliveryError, QueueExpiry, QueueExpiryAttempts, QueueExpiryTtl, QueuedMessage,
QueuedRecipient, RecipientStatus, ServerResponse, QueuedRecipient, RecipientStatus, ServerResponse,
@@ -40,7 +40,7 @@ use std::str::FromStr;
use store::{ use store::{
Deserialize, IterateParams, U64_LEN, ValueKey, Deserialize, IterateParams, U64_LEN, ValueKey,
ahash::AHashSet, ahash::AHashSet,
registry::{RegistryFilterOp, RegistryQuery}, registry::RegistryFilterOp,
write::{AlignedBytes, Archive, QueueClass, ValueClass, key::DeserializeBigEndian, now}, write::{AlignedBytes, Archive, QueueClass, ValueClass, key::DeserializeBigEndian, now},
}; };
use trc::AddContext; use trc::AddContext;
+6 -2
View File
@@ -29,9 +29,13 @@ impl EnterpriseRegistry for Server {
} }
// These are the Enterprise features INBUXA hasn't rebuilt yet
// (docs/spec/SPEC.md §4). Each type leaves this list when its rebuild
// lands. There's no edition to upgrade to, so the message says so.
Err(trc::JmapEvent::Forbidden.into_err().details(concat!( Err(trc::JmapEvent::Forbidden.into_err().details(concat!(
"This feature is only available in the Enterprise edition. ", "This feature isn't available in ",
"Obtain your trial license at https://license.stalw.art/trial." types::brand!(),
" yet."
))) )))
} }
} }
@@ -11,7 +11,6 @@ use groupware::{cache::GroupwareCache, calendar::CalendarEvent, contact::Contact
use registry::{ use registry::{
schema::{ schema::{
enums::IndexDocumentType, enums::IndexDocumentType,
prelude::{ObjectType, Property},
structs::{TaskIndexDocument, TaskIndexTrace, TaskStatus}, structs::{TaskIndexDocument, TaskIndexTrace, TaskStatus},
}, },
types::EnumImpl, types::EnumImpl,
@@ -29,7 +28,6 @@ use store::{
}; };
use trc::{AddContext, TaskManagerEvent}; use trc::{AddContext, TaskManagerEvent};
use types::{ use types::{
blob_hash::BlobHash,
collection::{Collection, SyncCollection}, collection::{Collection, SyncCollection},
field::EmailField, field::EmailField,
}; };
+15 -1
View File
@@ -18,7 +18,21 @@ impl RegistryStore {
// Build store // Build store
inner.store = match inner.read_data_store().await { inner.store = match inner.read_data_store().await {
RegistryInit::Ok(data_store) => Store::build(data_store).await?, RegistryInit::Ok(data_store) => {
// The recovery administrator exists for first-boot setup and
// for recovery mode. On a configured server running normally
// it would be a standing full-admin login for whoever knows the
// variable, so it's ignored there, and loudly.
if !inner.env_recovery_mode && inner.env_recovery_admin.take().is_some() {
eprintln!();
eprintln!("⚠️ INBUXA_RECOVERY_ADMIN (or STALWART_RECOVERY_ADMIN) is set, but the");
eprintln!(" server is configured and not in recovery mode, so it is ignored.");
eprintln!(" Remove it from the environment. To use it for recovery, also set");
eprintln!(" INBUXA_RECOVERY_MODE=1.");
eprintln!();
}
Store::build(data_store).await?
}
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;
+6 -3
View File
@@ -10,9 +10,14 @@ use common::enterprise::llm::{
}; };
#[cfg(feature = "pending-rebuild")] #[cfg(feature = "pending-rebuild")]
use spam_filter::analysis::llm::SpamFilterAnalyzeLlm; use spam_filter::analysis::llm::SpamFilterAnalyzeLlm;
#[cfg(feature = "pending-rebuild")]
use crate::utils::http_server::{HttpMessage, spawn_mock_http_server};
#[cfg(feature = "pending-rebuild")]
use http_proto::{JsonResponse, ToHttpResponse};
#[cfg(feature = "pending-rebuild")]
use hyper::Method;
use crate::utils::{ use crate::utils::{
dns::DnsCache, dns::DnsCache,
http_server::{HttpMessage, spawn_mock_http_server},
server::TestServerBuilder, server::TestServerBuilder,
}; };
use ahash::AHashSet; use ahash::AHashSet;
@@ -21,8 +26,6 @@ use common::{
auth::{AccountCache, AccountInfo}, auth::{AccountCache, AccountInfo},
config::mailstore::spamfilter::SpamFilterAction, config::mailstore::spamfilter::SpamFilterAction,
}; };
use http_proto::{JsonResponse, ToHttpResponse};
use hyper::Method;
use mail_auth::{ use mail_auth::{
ArcOutput, DkimOutput, DkimResult, DmarcResult, DnssecStatus, IprevOutput, IprevResult, MX, ArcOutput, DkimOutput, DkimResult, DmarcResult, DnssecStatus, IprevOutput, IprevResult, MX,
SpfOutput, SpfResult, dkim::Signature, dmarc::Policy, SpfOutput, SpfResult, dkim::Signature, dmarc::Policy,