Rename the identifiers that carried the upstream name
ci / fork-checks (pull_request) Successful in 16s
ci / build (pull_request) Successful in 7m53s

Everything clients, users and operators meet now carries the fork's name,
with no aliases (SPEC.md §2.4, changed here from "protocol identifiers
stay"):

- JMAP: upstream's registry capability is urn:inbuxa:jmap:registry, beside
  the fork's own urn:inbuxa:jmap.
- WebDAV lock and sync tokens are urn:inbuxa:dav*; clients resync once.
- Sieve: vnd.inbuxa.while and vnd.inbuxa.expressions. sieve-rs spells these
  into its compiler, so it's vendored (vendor/sieve-rs, 0.7.3) and patched in;
  a unit test fails if Cargo.lock ever moves past the vendored copy. The
  trusted runtime now names itself too, rather than answering sieve-rs's
  default.
- The web interface's OAuth client is inbuxa-webui. On every start the old
  stalwart-webui client is removed and any application naming it is moved
  over.
- The spam filter's blobs are INBUXA_SPAM_*; every start moves any left
  under the old keys, so a trained model survives.
- SQL stores and log files default to inbuxa, in the code and in the
  schema served to the admin (checksum regenerated).
- Settings are INBUXA_* only. A STALWART_* variable that's set where its
  INBUXA_* one isn't stops the server at startup, naming it.
- The version-upgrade messages link docs.inbuxa.org's migration page, and
  the OpenAPI description, smtp crate metadata and web-push test fixtures
  lose the name.

Kept on purpose, allowlisted with reasons: the OAuth key-derivation
contexts (renaming them would end every session and invalidate every
sealed client id) and the hashed application prefix.

Also fixes a latent start-up failure: ensure_client updated an existing
first-party client with a revision of 0, which the registry's assertion
never matches, so adding a redirect URI or changing the webmail secret
failed start-up. And the principal session test now expects
legacyProtocols (C-1, added 2026-09-21), which it had missed.

Tested: the server builds without warnings; common's 106 unit tests,
including the vendoring check; a new integration test for the two
start-up migrations; and the webdav, jmap, imap and SMTP Sieve suites.
This commit is contained in:
2026-09-22 19:33:02 -07:00
parent 4799d191a0
commit cc6f1eb298
129 changed files with 20504 additions and 168 deletions
+199
View File
@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Context, Envelope, Event,
compiler::{
VariableType,
grammar::actions::action_set::{Modifier, Set},
},
runtime::Variable,
};
use std::fmt::Write;
impl Set {
pub(crate) fn exec(&self, ctx: &mut Context) {
let mut value = ctx.eval_value(&self.value);
for modifier in &self.modifiers {
value = modifier.apply(value.to_string().as_ref(), ctx).into();
}
ctx.set_variable(&self.name, value);
}
}
impl Context<'_> {
pub(crate) fn set_variable(&mut self, var_name: &VariableType, mut variable: Variable) {
if variable.len() > self.runtime.max_variable_size {
let mut new_variable = String::with_capacity(self.runtime.max_variable_size);
for ch in variable.to_string().chars() {
if ch.len_utf8() + new_variable.len() <= self.runtime.max_variable_size {
new_variable.push(ch);
} else {
break;
}
}
variable = new_variable.into();
}
match var_name {
VariableType::Local(var_id) => {
if let Some(var) = self.vars_local.get_mut(*var_id) {
*var = variable.clone();
} else {
debug_assert!(false, "Non-existent local variable {var_id}");
}
}
VariableType::Global(var_name) => {
self.vars_global
.insert(var_name.to_string().into(), variable.clone());
}
VariableType::Envelope(env) => {
self.add_set_envelope_event(*env, variable.to_string().into_owned());
}
_ => (),
}
}
pub(crate) fn add_set_envelope_event(&mut self, envelope: Envelope, value: String) {
let mut did_find = false;
for (name, val) in self.envelope.iter_mut() {
if *name == envelope {
*val = Variable::String(value.clone().into());
did_find = true;
break;
}
}
if !did_find {
self.envelope
.push((envelope, Variable::String(value.clone().into())));
}
self.queued_events = vec![Event::SetEnvelope { envelope, value }].into_iter();
}
pub(crate) fn get_variable(&self, var_name: &VariableType) -> Option<&Variable> {
match var_name {
VariableType::Local(var_id) => self.vars_local.get(*var_id),
VariableType::Global(var_name) => self.vars_global.get(var_name.as_str()),
VariableType::Envelope(env) => self
.envelope
.iter()
.find_map(|(name, val)| if name == env { Some(val) } else { None }),
_ => unreachable!(),
}
}
}
impl Modifier {
pub(crate) fn apply(&self, input: &str, ctx: &Context) -> String {
let max_len = ctx.runtime.max_variable_size;
match self {
Modifier::Lower => input.to_lowercase(),
Modifier::Upper => input.to_uppercase(),
Modifier::LowerFirst => {
let mut result = String::with_capacity(input.len());
for (pos, char) in input.chars().enumerate() {
if result.len() + char.len_utf8() <= max_len {
if pos != 0 {
result.push(char);
} else {
for char in char.to_lowercase() {
result.push(char);
}
}
} else {
return result;
}
}
result
}
Modifier::UpperFirst => {
let mut result = String::with_capacity(input.len());
for (pos, char) in input.chars().enumerate() {
if result.len() + char.len_utf8() <= max_len {
if pos != 0 {
result.push(char);
} else {
for char in char.to_uppercase() {
result.push(char);
}
}
} else {
return result;
}
}
result
}
Modifier::QuoteWildcard => {
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if ['*', '\\', '?'].contains(&char) {
if result.len() + char.len_utf8() < max_len {
result.push('\\');
result.push(char);
} else {
return result;
}
} else if result.len() + char.len_utf8() <= max_len {
result.push(char);
} else {
return result;
}
}
result
}
Modifier::QuoteRegex => {
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if [
'*', '\\', '?', '.', '[', ']', '(', ')', '+', '{', '}', '|', '^', '=', ':',
'$',
]
.contains(&char)
{
if result.len() + char.len_utf8() < max_len {
result.push('\\');
result.push(char);
} else {
return result;
}
} else if result.len() + char.len_utf8() <= max_len {
result.push(char);
} else {
return result;
}
}
result
}
Modifier::Length => input.chars().count().to_string(),
Modifier::EncodeUrl => {
let mut buf = [0; 4];
let mut result = String::with_capacity(input.len());
for char in input.chars() {
if char.is_ascii_alphanumeric() || ['-', '.', '_', '~'].contains(&char) {
if result.len() < max_len {
result.push(char);
} else {
return result;
}
} else if result.len() + (char.len_utf8() * 3) <= max_len {
for byte in char.encode_utf8(&mut buf).as_bytes().iter() {
write!(result, "%{byte:02x}").ok();
}
} else {
return result;
}
}
result
}
Modifier::Replace { find, replace } => input.replace(
ctx.eval_value(find).to_string().as_ref(),
ctx.eval_value(replace).to_string().as_ref(),
),
}
}
}