Rename the identifiers that carried the upstream name
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:
@@ -49,6 +49,7 @@ email = { path = "../crates/email", features = ["test_mode"] }
|
||||
spam-filter = { path = "../crates/spam-filter", features = ["test_mode"] }
|
||||
trc = { path = "../crates/trc", features = [] }
|
||||
managesieve = { path = "../crates/managesieve", features = ["test_mode"] }
|
||||
migration = { path = "../crates/migration" }
|
||||
smtp-proto = { version = "0.2" }
|
||||
mail-auth = { version = "0.13", features = ["test"] }
|
||||
mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] }
|
||||
|
||||
@@ -105,7 +105,7 @@ def stop():
|
||||
docker("rm", "-f", NAME, check_rc=False)
|
||||
|
||||
|
||||
def jmap(user, password, calls, using=("urn:ietf:params:jmap:core", "urn:stalwart:jmap", INBUXA)):
|
||||
def jmap(user, password, calls, using=("urn:ietf:params:jmap:core", "urn:inbuxa:jmap:registry", INBUXA)):
|
||||
body = json.dumps({"using": list(using), "methodCalls": calls}).encode()
|
||||
req = urllib.request.Request(f"{HTTP}/jmap/", data=body, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["fileinto", "mailbox", "mailboxid", "special-use", "ihave", "imap4flags", "vnd.stalwart.expressions"];
|
||||
require ["fileinto", "mailbox", "mailboxid", "special-use", "ihave", "imap4flags", "vnd.inbuxa.expressions"];
|
||||
|
||||
# SpecialUse extension tests
|
||||
if not specialuse_exists ["inbox", "trash"] {
|
||||
|
||||
@@ -8,7 +8,7 @@ import urllib.error
|
||||
import urllib.request
|
||||
|
||||
CORE = "urn:ietf:params:jmap:core"
|
||||
STALWART = "urn:stalwart:jmap"
|
||||
STALWART = "urn:inbuxa:jmap:registry"
|
||||
USING = [CORE, STALWART]
|
||||
|
||||
DEFAULT_BASE_URL = "https://127.0.0.1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["variables", "include", "vnd.stalwart.expressions", "reject"];
|
||||
require ["variables", "include", "vnd.inbuxa.expressions", "reject"];
|
||||
|
||||
global "score";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["variables", "include", "vnd.stalwart.expressions", "reject"];
|
||||
require ["variables", "include", "vnd.inbuxa.expressions", "reject"];
|
||||
|
||||
global "score";
|
||||
set "awl_factor" "0.5";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["variables", "envelope", "reject", "vnd.stalwart.expressions"];
|
||||
require ["variables", "envelope", "reject", "vnd.inbuxa.expressions"];
|
||||
|
||||
if envelope :localpart :is "from" "spammer" {
|
||||
reject "450 4.1.1 Invalid address";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["variables", "envelope", "reject", "vnd.stalwart.expressions"];
|
||||
require ["variables", "envelope", "reject", "vnd.inbuxa.expressions"];
|
||||
|
||||
if envelope :domain :is "to" "foobar.org" {
|
||||
eval "query('sql', 'CREATE TABLE IF NOT EXISTS greylist (addr TEXT PRIMARY KEY)', [])";
|
||||
|
||||
@@ -250,9 +250,10 @@ pub async fn test(test: &TestServer) {
|
||||
"webWriteUrlTemplate": null
|
||||
},
|
||||
"urn:ietf:params:jmap:mail:share": {},
|
||||
"urn:stalwart:jmap": {},
|
||||
// inbuxa: MT-22, the logo that applies to the account
|
||||
"urn:inbuxa:jmap": { "logo": null },
|
||||
"urn:inbuxa:jmap:registry": {},
|
||||
// inbuxa: MT-22, the logo that applies to the account, and
|
||||
// LP-19, whether the legacy protocols are open to it
|
||||
"urn:inbuxa:jmap": { "logo": null, "legacyProtocols": "enabled" },
|
||||
"https://www.fastmail.com/dev/maskedemail": {}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +275,7 @@ pub async fn test(test: &TestServer) {
|
||||
"urn:ietf:params:jmap:principals:availability": john_id,
|
||||
"urn:ietf:params:jmap:filenode": john_id,
|
||||
"urn:ietf:params:jmap:mail:share": john_id,
|
||||
"urn:stalwart:jmap": john_id,
|
||||
"urn:inbuxa:jmap:registry": john_id,
|
||||
"https://www.fastmail.com/dev/maskedemail": john_id
|
||||
},
|
||||
"username": "[email protected]",
|
||||
|
||||
@@ -30,6 +30,8 @@ pub mod imap;
|
||||
#[cfg(test)]
|
||||
pub mod jmap;
|
||||
#[cfg(test)]
|
||||
pub mod renamed_identifiers; // inbuxa: SPEC.md §2.4
|
||||
#[cfg(test)]
|
||||
pub mod scim;
|
||||
#[cfg(test)]
|
||||
pub mod smtp;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! inbuxa: what an install from before the rename carries over (SPEC.md
|
||||
//! §2.4). The web interface's OAuth client is retired rather than kept as an
|
||||
//! alias, and a trained spam filter moves to its new keys.
|
||||
|
||||
use crate::utils::server::TestServerBuilder;
|
||||
use common::manager::{SPAM_CLASSIFIER_KEY, SPAM_TRAINER_KEY, first_party::WEB_INTERFACE_CLIENT_ID};
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::CompressionAlgo,
|
||||
prelude::{ObjectType, Property},
|
||||
structs::{Application, OAuthClient},
|
||||
},
|
||||
types::map::Map,
|
||||
};
|
||||
|
||||
const OLD_CLIENT_ID: &str = "stalwart-webui";
|
||||
const OLD_TRAINER_KEY: &[u8] = b"STALWART_SPAM_TRAIN_DATA.lz4";
|
||||
const OLD_CLASSIFIER_KEY: &[u8] = b"STALWART_SPAM_CLASSIFIER_MODEL.lz4";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn renamed_identifiers() {
|
||||
// The web interface registered under upstream's client id, and an
|
||||
// application naming it. Disabled, so nothing is fetched for it.
|
||||
let test = TestServerBuilder::new("renamed_identifiers")
|
||||
.await
|
||||
.with_object(OAuthClient {
|
||||
client_id: OLD_CLIENT_ID.to_string(),
|
||||
redirect_uris: Map::new(vec!["https://127.0.0.1/admin/oauth/callback".to_string()]),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.with_object(Application {
|
||||
enabled: false,
|
||||
description: "Web interface".to_string(),
|
||||
resource_url: "https://127.0.0.1/webui.zip".to_string(),
|
||||
url_prefix: Map::new(vec!["/admin".to_string()]),
|
||||
oauth_client_id: Some(OLD_CLIENT_ID.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.disable_services()
|
||||
.build()
|
||||
.await;
|
||||
|
||||
println!("Running renamed identifier tests...");
|
||||
let registry = test.server.registry();
|
||||
assert_eq!(
|
||||
registry
|
||||
.primary_key(
|
||||
ObjectType::OAuthClient.into(),
|
||||
Property::ClientId,
|
||||
OLD_CLIENT_ID.as_bytes().to_vec(),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
None,
|
||||
"the pre-rename web interface client is retired on start"
|
||||
);
|
||||
let applications = registry.list::<Application>().await.unwrap();
|
||||
assert_eq!(applications.len(), 1);
|
||||
assert_eq!(
|
||||
applications[0].object.oauth_client_id.as_deref(),
|
||||
Some(WEB_INTERFACE_CLIENT_ID),
|
||||
"an application naming the old client moves to the new one"
|
||||
);
|
||||
|
||||
// A trained model under the pre-rename keys moves to the new ones.
|
||||
let blobs = test.server.blob_store();
|
||||
for (key, data) in [
|
||||
(OLD_TRAINER_KEY, &b"trainer"[..]),
|
||||
(OLD_CLASSIFIER_KEY, &b"classifier"[..]),
|
||||
] {
|
||||
blobs.put_blob(key, data, CompressionAlgo::None).await.unwrap();
|
||||
}
|
||||
migration::try_migrate(&test.server).await.unwrap();
|
||||
for (old, new, data) in [
|
||||
(OLD_TRAINER_KEY, SPAM_TRAINER_KEY, &b"trainer"[..]),
|
||||
(OLD_CLASSIFIER_KEY, SPAM_CLASSIFIER_KEY, &b"classifier"[..]),
|
||||
] {
|
||||
assert_eq!(blobs.get_blob(new, 0..usize::MAX).await.unwrap().as_deref(), Some(data));
|
||||
assert_eq!(blobs.get_blob(old, 0..usize::MAX).await.unwrap(), None);
|
||||
}
|
||||
|
||||
// A blob already under the new key wins over a stale old one.
|
||||
blobs
|
||||
.put_blob(OLD_CLASSIFIER_KEY, b"stale", CompressionAlgo::None)
|
||||
.await
|
||||
.unwrap();
|
||||
migration::try_migrate(&test.server).await.unwrap();
|
||||
assert_eq!(
|
||||
blobs
|
||||
.get_blob(SPAM_CLASSIFIER_KEY, 0..usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some(&b"classifier"[..])
|
||||
);
|
||||
assert_eq!(blobs.get_blob(OLD_CLASSIFIER_KEY, 0..usize::MAX).await.unwrap(), None);
|
||||
|
||||
// With nothing left to move, starting again changes nothing.
|
||||
migration::try_migrate(&test.server).await.unwrap();
|
||||
assert_eq!(
|
||||
blobs.get_blob(SPAM_TRAINER_KEY, 0..usize::MAX).await.unwrap().as_deref(),
|
||||
Some(&b"trainer"[..])
|
||||
);
|
||||
}
|
||||
@@ -479,7 +479,7 @@ pub async fn test(test: &mut TestServer) {
|
||||
.await;
|
||||
stub.set(Mode::Echo);
|
||||
user.activate_script(concat!(
|
||||
"require [\"vnd.stalwart.expressions\", \"editheader\", \"variables\"];\n",
|
||||
"require [\"vnd.inbuxa.expressions\", \"editheader\", \"variables\"];\n",
|
||||
"let \"a\" \"llm_prompt('echo-test', 'hello world', 0.5)\";\n",
|
||||
"let \"b\" \"llm_prompt('no-such-model', 'x', 0.5)\";\n",
|
||||
"addheader \"X-Llm-Echo\" \"${a}\";\n",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use crate::utils::account::Account;
|
||||
@@ -408,7 +410,7 @@ impl Account {
|
||||
"urn:ietf:params:jmap:principals:availability",
|
||||
"urn:ietf:params:jmap:filenode",
|
||||
"urn:ietf:params:jmap:mail:share",
|
||||
"urn:stalwart:jmap"
|
||||
"urn:inbuxa:jmap:registry"
|
||||
],
|
||||
"methodCalls": calls
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use crate::utils::{server::TestServer, webdav::GenerateTestDavResource};
|
||||
@@ -45,7 +47,7 @@ pub async fn test(test: &TestServer) {
|
||||
|
||||
// Test 2: Refreshing a lock token with an invalid a lock token should fail
|
||||
client
|
||||
.lock_refresh(&path, "urn:stalwart:davlock:1234", "infinity", "Second-456")
|
||||
.lock_refresh(&path, "urn:inbuxa:davlock:1234", "infinity", "Second-456")
|
||||
.await
|
||||
.with_status(StatusCode::PRECONDITION_FAILED);
|
||||
|
||||
@@ -148,7 +150,7 @@ pub async fn test(test: &TestServer) {
|
||||
|
||||
// Test 10: Unlock with and without a lock token
|
||||
client
|
||||
.unlock(&path, "urn:stalwart:davlock:1234")
|
||||
.unlock(&path, "urn:inbuxa:davlock:1234")
|
||||
.await
|
||||
.with_status(StatusCode::CONFLICT)
|
||||
.with_value("D:error.D:lock-token-matches-request-uri", "");
|
||||
|
||||
Reference in New Issue
Block a user