From 5931b06392dc721291dbb7e256cca6f652da29ea Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 10:06:08 -0700 Subject: [PATCH 1/6] Rename to inbuxa-migrate The crate, binary, library path and every name the tool writes or reads become inbuxa-migrate: the CLI name and about text, the HTTP user agent, the credential variables (INBUXA_MIGRATE_PASSWORD, _TOKEN, _EWS_CLIENT_SECRET and _GRAPH_TOKEN, with no fallback to the old names), thread names, synthesized UIDs and calendar addresses (urn:x-inbuxa-migrate:attendee:), and the Maildir file suffix used by the export script. This is a new tool, so archives written by Vandelay are not read back. The version moves to the date scheme the release tags use (2026.9.30), and the package metadata points at the inbuxa-migrate repository. The cargo-dist and MSI metadata goes with the release tooling it served. Modified files carry an added copyright line; the original notices stay. --- Cargo.lock | 64 ++-- Cargo.toml | 24 +- scripts/export_archive.py | 10 +- src/cli.rs | 81 ++--- src/dav/client.rs | 3 +- src/exchange/jscalendar.rs | 9 +- src/exchange_ews/calendar_map.rs | 11 +- src/exchange_ews/client.rs | 3 +- src/exchange_ews/contact_map.rs | 5 +- src/exchange_ews/parse.rs | 3 +- src/exchange_graph/calendar_map.rs | 7 +- src/exchange_graph/client.rs | 3 +- src/exchange_graph/contact_map.rs | 5 +- src/exchange_graph/types.rs | 3 +- src/jmap/session.rs | 3 +- src/main.rs | 9 +- src/sync/import_dav/calcard.rs | 5 +- src/sync/import_exchange_ews/coordinator.rs | 3 +- src/sync/import_exchange_graph/coordinator.rs | 3 +- src/sync/import_managesieve/coordinator.rs | 5 +- tests/integration/baikal.rs | 9 +- tests/integration/cyrus.rs | 18 +- tests/integration/data.rs | 89 +++--- tests/integration/dovecot.rs | 25 +- tests/integration/layouts.rs | 3 +- tests/integration/mod.rs | 3 +- tests/integration/validate.rs | 7 +- tests/integration/webdav.rs | 3 +- tests/integration_baikal.rs | 7 +- tests/integration_cyrus.rs | 13 +- tests/integration_dovecot.rs | 21 +- tests/integration_radicale.rs | 7 +- tests/integration_webdav.rs | 9 +- tests/live_stalwart.rs | 7 +- tests/mock_dav.rs | 117 ++++--- tests/mock_exchange_ews.rs | 77 ++--- tests/mock_exchange_graph.rs | 298 +++++++++--------- tests/mock_imap.rs | 31 +- tests/mock_jmap.rs | 43 +-- tests/mock_maildir.rs | 21 +- tests/mock_managesieve.rs | 21 +- tests/mock_sync.rs | 29 +- tests/seed_smoke.rs | 5 +- tests/seeder/mod.rs | 9 +- tests/seeder/seed.rs | 13 +- tests/sync_caldav.rs | 13 +- tests/sync_carddav.rs | 13 +- tests/sync_exchange_graph.rs | 25 +- tests/sync_imap.rs | 13 +- tests/sync_jmap.rs | 79 ++--- tests/sync_maildir.rs | 21 +- tests/sync_managesieve.rs | 27 +- tests/sync_takeout.rs | 15 +- tests/sync_webdav.rs | 13 +- 54 files changed, 719 insertions(+), 644 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c86ec9..79e288f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1323,6 +1323,38 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "inbuxa-migrate" +version = "2026.9.30" +dependencies = [ + "blake3", + "calcard", + "chrono", + "chrono-tz", + "clap", + "crossbeam-channel", + "encodify", + "flate2", + "indexmap 2.14.2", + "mail-parser", + "mockito", + "num_cpus", + "psl", + "quick-xml", + "regex", + "rusqlite", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "tempfile", + "testcontainers", + "thiserror", + "time", + "ureq", + "url", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2936,38 +2968,6 @@ dependencies = [ "sha1_smol", ] -[[package]] -name = "vandelay" -version = "1.0.11" -dependencies = [ - "blake3", - "calcard", - "chrono", - "chrono-tz", - "clap", - "crossbeam-channel", - "encodify", - "flate2", - "indexmap 2.14.2", - "mail-parser", - "mockito", - "num_cpus", - "psl", - "quick-xml", - "regex", - "rusqlite", - "rustls", - "rustls-platform-verifier", - "serde", - "serde_json", - "tempfile", - "testcontainers", - "thiserror", - "time", - "ureq", - "url", -] - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index c3b8088..e840c03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,16 @@ [package] -name = "vandelay" -description = "JMAP account migration utility" -version = "1.0.11" -authors = ["Stalwart Labs LLC "] +name = "inbuxa-migrate" +description = "Account migration and backup for inbuxa" +version = "2026.9.30" +authors = ["John Coffey "] license = "Apache-2.0 OR MIT" -repository = "https://github.com/stalwartlabs/vandelay" -homepage = "https://github.com/stalwartlabs/vandelay" +repository = "https://git.coffeylabs.org/inbuxa/inbuxa-migrate" +homepage = "https://git.coffeylabs.org/inbuxa/inbuxa-migrate" edition = "2024" readme = "README.md" -[package.metadata.wix] -upgrade-guid = "5C8148E7-34A9-4216-A802-FFB8345AECC4" -path-guid = "FA9E97EB-5E5C-4809-9018-9DC1EDB0E93D" -license = false -eula = false - [[bin]] -name = "vandelay" +name = "inbuxa-migrate" path = "src/main.rs" [features] @@ -52,7 +46,3 @@ psl = "2.1.226" mockito = "1.7.2" tempfile = "3.13" testcontainers = { version = "0.28", features = ["blocking"] } - -[profile.dist] -inherits = "release" -lto = "thin" diff --git a/scripts/export_archive.py b/scripts/export_archive.py index effc123..9d8a958 100644 --- a/scripts/export_archive.py +++ b/scripts/export_archive.py @@ -1,4 +1,10 @@ #!/usr/bin/env python3 +# +# SPDX-FileCopyrightText: 2020 Stalwart Labs LLC +# SPDX-FileCopyrightText: 2026 John Coffey +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# import argparse import json @@ -136,7 +142,7 @@ def export_mail(conn, root): if directory is None: continue sequence += 1 - name = f"{stamp}.M{email_id}P{sequence}.vandelay:2,{info}" + name = f"{stamp}.M{email_id}P{sequence}.inbuxa-migrate:2,{info}" with open(os.path.join(directory, "cur", name), "wb") as handle: handle.write(data) written += 1 @@ -320,7 +326,7 @@ def export_files(conn, root): def main(): parser = argparse.ArgumentParser( - description="Export a Vandelay SQLite archive to a directory tree." + description="Export an inbuxa-migrate SQLite archive to a directory tree." ) parser.add_argument("archive") parser.add_argument("target") diff --git a/src/cli.rs b/src/cli.rs index 91855c6..92d5370 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -30,9 +31,9 @@ use crate::types::{ObjectType, parse_object_list}; #[derive(Parser)] #[command( - name = "vandelay", + name = "inbuxa-migrate", version, - about = "Vandelay: the JMAP importer-exporter" + about = "inbuxa-migrate: account migration and backup for inbuxa" )] pub struct Cli { #[command(subcommand)] @@ -138,7 +139,7 @@ struct JmapImportArgs { #[arg( long, value_name = "PASS", - help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)" + help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)" )] auth_password: Option, @@ -146,7 +147,7 @@ struct JmapImportArgs { long, value_name = "TOKEN", num_args = 0..=1, - help = "Bearer token (value optional: falls back to $VANDELAY_TOKEN or prompt)" + help = "Bearer token (value optional: falls back to $INBUXA_MIGRATE_TOKEN or prompt)" )] auth_bearer: Option>, @@ -199,7 +200,7 @@ struct ImapImportArgs { #[arg( long, value_name = "PASS", - help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)" + help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)" )] auth_password: Option, @@ -207,7 +208,7 @@ struct ImapImportArgs { long, value_name = "TOKEN", num_args = 0..=1, - help = "Bearer token for XOAUTH2 (value optional: falls back to $VANDELAY_TOKEN or prompt)" + help = "Bearer token for XOAUTH2 (value optional: falls back to $INBUXA_MIGRATE_TOKEN or prompt)" )] auth_bearer: Option>, @@ -309,7 +310,7 @@ struct ExportArgs { #[arg( long, value_name = "PASS", - help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)" + help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)" )] auth_password: Option, @@ -317,7 +318,7 @@ struct ExportArgs { long, value_name = "TOKEN", num_args = 0..=1, - help = "Bearer token (value optional: falls back to $VANDELAY_TOKEN or prompt)" + help = "Bearer token (value optional: falls back to $INBUXA_MIGRATE_TOKEN or prompt)" )] auth_bearer: Option>, @@ -407,7 +408,7 @@ pub struct ManageSieveImportArgs { #[arg( long, value_name = "PASS", - help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)" + help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)" )] auth_password: Option, @@ -415,7 +416,7 @@ pub struct ManageSieveImportArgs { long, value_name = "TOKEN", num_args = 0..=1, - help = "Bearer token for OAUTHBEARER (value optional: $VANDELAY_TOKEN or prompt)" + help = "Bearer token for OAUTHBEARER (value optional: $INBUXA_MIGRATE_TOKEN or prompt)" )] auth_bearer: Option>, @@ -457,7 +458,7 @@ pub struct DavImportArgs { #[arg( long, value_name = "PASS", - help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)" + help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)" )] auth_password: Option, @@ -465,7 +466,7 @@ pub struct DavImportArgs { long, value_name = "TOKEN", num_args = 0..=1, - help = "Bearer token (value optional: $VANDELAY_TOKEN or prompt)" + help = "Bearer token (value optional: $INBUXA_MIGRATE_TOKEN or prompt)" )] auth_bearer: Option>, @@ -560,7 +561,7 @@ fn resolve_managesieve_auth( "--auth-user is only valid with --auth-bearer".to_owned(), )); } - let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?; + let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?; return Ok(ManageSieveAuth::Basic { user: user.to_owned(), password, @@ -577,7 +578,7 @@ fn resolve_managesieve_auth( let user = auth_user .ok_or_else(|| Error::Usage("--auth-user is required with --auth-bearer".to_owned()))? .to_owned(); - let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?; + let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?; Ok(ManageSieveAuth::Bearer { user, token }) } @@ -726,7 +727,7 @@ fn resolve_dav_auth( auth_bearer: Option<&Option>, ) -> Result { if let Some(user) = auth_basic { - let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?; + let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?; return Ok(DavAuth::Basic { user: user.to_owned(), password, @@ -740,7 +741,7 @@ fn resolve_dav_auth( let bearer = auth_bearer.ok_or_else(|| { Error::Usage("exactly one of --auth-basic / --auth-bearer is required".to_owned()) })?; - let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?; + let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?; Ok(DavAuth::Bearer { token }) } @@ -828,7 +829,7 @@ fn resolve_imap_auth( "--auth-user is only valid with --auth-bearer".to_owned(), )); } - let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?; + let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?; return Ok(ImapAuth::Basic { user: user.to_owned(), password, @@ -845,7 +846,7 @@ fn resolve_imap_auth( let user = auth_user .ok_or_else(|| Error::Usage("--auth-user is required with --auth-bearer".to_owned()))? .to_owned(); - let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?; + let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?; Ok(ImapAuth::Bearer { user, token }) } @@ -921,7 +922,7 @@ pub struct ExchangeEwsImportArgs { #[arg( long, value_name = "PASS", - help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)" + help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)" )] auth_password: Option, @@ -929,7 +930,7 @@ pub struct ExchangeEwsImportArgs { long, value_name = "TOKEN", num_args = 0..=1, - help = "Bearer token (value optional: $VANDELAY_TOKEN or device-code flow)" + help = "Bearer token (value optional: $INBUXA_MIGRATE_TOKEN or device-code flow)" )] auth_bearer: Option>, @@ -946,7 +947,7 @@ pub struct ExchangeEwsImportArgs { #[arg( long, value_name = "SECRET", - help = "OAuth client secret (app-only flow; prefer $VANDELAY_EWS_CLIENT_SECRET)" + help = "OAuth client secret (app-only flow; prefer $INBUXA_MIGRATE_EWS_CLIENT_SECRET)" )] ews_client_secret: Option, @@ -1029,7 +1030,7 @@ fn resolve_exchange_ews_auth(args: &ExchangeEwsImportArgs) -> Result Result Result Result>, @@ -1168,7 +1169,7 @@ pub struct ExchangeGraphImportArgs { help = "Years either side of today to scan for edited recurrence occurrences (max 5)", long_help = "Years either side of today to scan for edited recurrence occurrences.\n \ Graph reports a series exception only inside an expanded calendar view, and caps\n \ - any single view at five years, so vandelay scans [today - N, today] and\n \ + any single view at five years, so inbuxa-migrate scans [today - N, today] and\n \ [today, today + N]. An occurrence edited outside that span imports with the\n \ series default instead of its edit." )] @@ -1177,7 +1178,7 @@ pub struct ExchangeGraphImportArgs { #[arg( long, help = "Skip contact photos (saves one request per contact on large address books)", - long_help = "Skip contact photos.\n Graph will not say which contacts have a photo: $expand=photo comes back empty\n even when one exists, so the only way to find out is to ask for the bytes.\n vandelay therefore spends one extra request per contact. Pass this to skip\n photos entirely on a large address book." + long_help = "Skip contact photos.\n Graph will not say which contacts have a photo: $expand=photo comes back empty\n even when one exists, so the only way to find out is to ask for the bytes.\n inbuxa-migrate therefore spends one extra request per contact. Pass this to skip\n photos entirely on a large address book." )] skip_contact_photos: bool, @@ -1265,7 +1266,11 @@ fn resolve_graph_auth( tenant: &str, ) -> Result { if let Some(slot) = access_token { - let token = secret::resolve(slot.as_deref(), "VANDELAY_GRAPH_TOKEN", "bearer token")?; + let token = secret::resolve( + slot.as_deref(), + "INBUXA_MIGRATE_GRAPH_TOKEN", + "bearer token", + )?; return Ok(GraphAuth::PreAcquired { token }); } let client_id = client_id @@ -1305,7 +1310,7 @@ fn resolve_auth( auth_bearer: Option<&Option>, ) -> Result { if let Some(user) = auth_basic { - let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?; + let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?; return Ok(Auth::Basic { user: user.to_owned(), password, @@ -1319,7 +1324,7 @@ fn resolve_auth( let bearer = auth_bearer.ok_or_else(|| { Error::Usage("exactly one of --auth-basic / --auth-bearer is required".to_owned()) })?; - let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?; + let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?; Ok(Auth::Bearer { token }) } @@ -1350,8 +1355,8 @@ mod tests { fn auth_password_without_basic_is_usage_error() { let _g = lock(); unsafe { - env::remove_var("VANDELAY_TOKEN"); - env::remove_var("VANDELAY_PASSWORD"); + env::remove_var("INBUXA_MIGRATE_TOKEN"); + env::remove_var("INBUXA_MIGRATE_PASSWORD"); } let bearer: Option = Some("t".to_owned()); let r = resolve_auth(None, Some("hunter2"), Some(&bearer)); @@ -1367,8 +1372,8 @@ mod tests { fn auth_basic_with_password_resolves() { let _g = lock(); unsafe { - env::remove_var("VANDELAY_TOKEN"); - env::remove_var("VANDELAY_PASSWORD"); + env::remove_var("INBUXA_MIGRATE_TOKEN"); + env::remove_var("INBUXA_MIGRATE_PASSWORD"); } let auth = resolve_auth(Some("alice"), Some("pw"), None).unwrap(); match auth { @@ -1384,8 +1389,8 @@ mod tests { fn auth_bearer_inline_token_resolves() { let _g = lock(); unsafe { - env::remove_var("VANDELAY_TOKEN"); - env::remove_var("VANDELAY_PASSWORD"); + env::remove_var("INBUXA_MIGRATE_TOKEN"); + env::remove_var("INBUXA_MIGRATE_PASSWORD"); } let bearer: Option = Some("t".to_owned()); let auth = resolve_auth(None, None, Some(&bearer)).unwrap(); @@ -1394,7 +1399,7 @@ mod tests { fn graph_config(objects: &[&str]) -> Result { let mut argv = vec![ - "vandelay", + "inbuxa-migrate", "import", "exchange-graph", "--access-token", diff --git a/src/dav/client.rs b/src/dav/client.rs index 2766060..c6ebe81 100644 --- a/src/dav/client.rs +++ b/src/dav/client.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -87,7 +88,7 @@ impl DavClient { log_level: AtomicU8::new(LEVEL_DEFAULT), retries_total: AtomicU64::new(0), retry_after_sleeps: AtomicU64::new(0), - user_agent: format!("vandelay/{}", env!("CARGO_PKG_VERSION")), + user_agent: format!("inbuxa-migrate/{}", env!("CARGO_PKG_VERSION")), }), } } diff --git a/src/exchange/jscalendar.rs b/src/exchange/jscalendar.rs index 0dbf2bf..ecdedd5 100644 --- a/src/exchange/jscalendar.rs +++ b/src/exchange/jscalendar.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -36,7 +37,7 @@ const CALENDAR_ADDRESS_DEPENDENTS: &[&str] = &[ pub fn synthetic_attendee_address(identifier: &str) -> String { format!( - "urn:x-vandelay:attendee:{}", + "urn:x-inbuxa-migrate:attendee:{}", blake3::hash(identifier.as_bytes()).to_hex() ) } @@ -146,7 +147,7 @@ mod tests { let event = json!({ "@type": "Event", "uid": "uid-1", - "prodId": "vandelay", + "prodId": "inbuxa-migrate", "privacy": "public", "organizerCalendarAddress": "mailto:alice@example.com", "recurrenceRule": {"frequency": "daily"}, @@ -173,7 +174,7 @@ mod tests { let addr = synthetic_attendee_address("Jane Doe"); assert_eq!(addr, synthetic_attendee_address("Jane Doe")); assert_ne!(addr, synthetic_attendee_address("John Doe")); - assert!(addr.starts_with("urn:x-vandelay:attendee:")); + assert!(addr.starts_with("urn:x-inbuxa-migrate:attendee:")); assert!( !addr.starts_with("mailto:"), "export must never invite a fabricated address" @@ -186,7 +187,7 @@ mod tests { "1": { "@type": "Participant", "name": "Room 4", - "calendarAddress": "urn:x-vandelay:attendee:abc", + "calendarAddress": "urn:x-inbuxa-migrate:attendee:abc", "email": "room4@example.com", "roles": {"required": true}, "kind": "resource", diff --git a/src/exchange_ews/calendar_map.rs b/src/exchange_ews/calendar_map.rs index 0c07345..0ee6537 100644 --- a/src/exchange_ews/calendar_map.rs +++ b/src/exchange_ews/calendar_map.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -53,7 +54,7 @@ fn build_event_map(raw: &CalendarItemRaw, iana: Option<&str>) -> Map + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -81,7 +82,7 @@ impl EwsClient { retries_total: AtomicU64::new(0), retry_after_sleeps: AtomicU64::new(0), soap_calls: AtomicU64::new(0), - user_agent: format!("vandelay/{}", env!("CARGO_PKG_VERSION")), + user_agent: format!("inbuxa-migrate/{}", env!("CARGO_PKG_VERSION")), }), } } diff --git a/src/exchange_ews/contact_map.rs b/src/exchange_ews/contact_map.rs index 632c3e7..e73342c 100644 --- a/src/exchange_ews/contact_map.rs +++ b/src/exchange_ews/contact_map.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -10,7 +11,7 @@ use crate::exchange_ews::parse::{ContactItemRaw, RawContactAddress}; pub fn synthetic_uid(item_id: &str) -> String { let hash = blake3::hash(item_id.as_bytes()).to_hex(); - format!("vandelay-ews-syn-{hash}") + format!("inbuxa-migrate-ews-syn-{hash}") } pub fn to_jscontact(raw: &ContactItemRaw) -> Value { @@ -389,7 +390,7 @@ mod tests { let u2 = synthetic_uid("ITEM-1"); assert_eq!(u1, u2); assert_ne!(u1, synthetic_uid("ITEM-2")); - assert!(u1.starts_with("vandelay-ews-syn-")); + assert!(u1.starts_with("inbuxa-migrate-ews-syn-")); } #[test] diff --git a/src/exchange_ews/parse.rs b/src/exchange_ews/parse.rs index 0024b54..eb12f67 100644 --- a/src/exchange_ews/parse.rs +++ b/src/exchange_ews/parse.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -724,7 +725,7 @@ fn wrap_inner_with_namespaces(inner: &str) -> String { return String::new(); } format!( - "{inner}", + "{inner}", ) } diff --git a/src/exchange_graph/calendar_map.rs b/src/exchange_graph/calendar_map.rs index c741020..c5f1994 100644 --- a/src/exchange_graph/calendar_map.rs +++ b/src/exchange_graph/calendar_map.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -701,7 +702,7 @@ mod tests { let p = &conv.data["participants"]["att-1"]; let addr = p["calendarAddress"].as_str().unwrap(); assert!( - addr.starts_with("urn:x-vandelay:attendee:"), + addr.starts_with("urn:x-inbuxa-migrate:attendee:"), "a hybrid X500 reference must not become a malformed mailto, got {addr}" ); assert!( @@ -755,7 +756,7 @@ mod tests { let p = &conv.data["participants"]["att-1"]; let addr = p["calendarAddress"].as_str().unwrap(); assert!( - addr.starts_with("urn:x-vandelay:attendee:"), + addr.starts_with("urn:x-inbuxa-migrate:attendee:"), "a name-only attendee needs a synthetic calendarAddress, got {addr}" ); assert!( @@ -799,7 +800,7 @@ mod tests { v["organizer"] = json!({"emailAddress": {"name": "Alice Example"}}); let conv = convert_event(&v, None).unwrap(); let organizer_address = conv.data["organizerCalendarAddress"].as_str().unwrap(); - assert!(organizer_address.starts_with("urn:x-vandelay:attendee:")); + assert!(organizer_address.starts_with("urn:x-inbuxa-migrate:attendee:")); assert_eq!( conv.data["participants"]["organizer"]["calendarAddress"], organizer_address diff --git a/src/exchange_graph/client.rs b/src/exchange_graph/client.rs index 7840b9e..40945ec 100644 --- a/src/exchange_graph/client.rs +++ b/src/exchange_graph/client.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -112,7 +113,7 @@ impl GraphClient { retries_total: AtomicU64::new(0), retry_after_sleeps: AtomicU64::new(0), requests_total: AtomicU64::new(0), - user_agent: format!("vandelay/{}", env!("CARGO_PKG_VERSION")), + user_agent: format!("inbuxa-migrate/{}", env!("CARGO_PKG_VERSION")), }), } } diff --git a/src/exchange_graph/contact_map.rs b/src/exchange_graph/contact_map.rs index 967c7a2..b48a36f 100644 --- a/src/exchange_graph/contact_map.rs +++ b/src/exchange_graph/contact_map.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -354,7 +355,7 @@ fn has_uri_scheme(value: &str) -> bool { pub fn synthetic_uid(graph_id: &str) -> String { let hash = blake3::hash(graph_id.as_bytes()); - format!("vandelay-graph-{}", hash.to_hex()) + format!("inbuxa-migrate-graph-{}", hash.to_hex()) } fn normalise_utc_datetime(raw: &str) -> String { @@ -465,7 +466,7 @@ mod tests { fn minimal_contact_yields_card_with_synthetic_uid() { let c = json!({"id": "GRAPH-1", "displayName": "Alice"}); let conv = convert_contact(&c).unwrap(); - assert!(conv.uid.starts_with("vandelay-graph-")); + assert!(conv.uid.starts_with("inbuxa-migrate-graph-")); assert_eq!(conv.data["@type"], "Card"); assert_eq!(conv.data["kind"], "individual"); assert_eq!(conv.data["name"]["full"], "Alice"); diff --git a/src/exchange_graph/types.rs b/src/exchange_graph/types.rs index 31bcde0..c7c3e23 100644 --- a/src/exchange_graph/types.rs +++ b/src/exchange_graph/types.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -18,7 +19,7 @@ impl MailboxKind { "primary" => Ok(MailboxKind::Primary), "archive" => Ok(MailboxKind::Archive), "public-folders" => Err(Error::Usage( - "Microsoft Graph does not expose public folders. Run `vandelay import exchange-ews --mailbox-kind public-folders` instead.".to_owned(), + "Microsoft Graph does not expose public folders. Run `inbuxa-migrate import exchange-ews --mailbox-kind public-folders` instead.".to_owned(), )), other => Err(Error::Usage(format!( "--mailbox-kind must be primary | archive, got {other:?}" diff --git a/src/jmap/session.rs b/src/jmap/session.rs index 6a1ccc1..32b584a 100644 --- a/src/jmap/session.rs +++ b/src/jmap/session.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -139,7 +140,7 @@ impl std::fmt::Display for OriginMismatch { } write!( f, - " on {} (connected to {}); vandelay must use the advertised URL. \ + " on {} (connected to {}); inbuxa-migrate must use the advertised URL. \ If this is wrong, fix the server's advertised HTTP URL setting.", self.session_origin, self.connect_origin ) diff --git a/src/main.rs b/src/main.rs index 9d35884..1b6f145 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use clap::Parser; -use vandelay::cli::{Action, Cli}; -use vandelay::error::Error; -use vandelay::inspect; -use vandelay::sync::{self, RunOutcome, Summary}; +use inbuxa_migrate::cli::{Action, Cli}; +use inbuxa_migrate::error::Error; +use inbuxa_migrate::inspect; +use inbuxa_migrate::sync::{self, RunOutcome, Summary}; fn main() { let code = run(); diff --git a/src/sync/import_dav/calcard.rs b/src/sync/import_dav/calcard.rs index fbf9c3e..72cd745 100644 --- a/src/sync/import_dav/calcard.rs +++ b/src/sync/import_dav/calcard.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -52,7 +53,7 @@ pub fn vcard_to_jscontact(text: &str, item_href: &str) -> Result String { let hash = blake3::hash(item_href.as_bytes()); - format!("vandelay-syn-{}", hash.to_hex()) + format!("inbuxa-migrate-syn-{}", hash.to_hex()) } #[derive(Debug)] @@ -208,7 +209,7 @@ END:VCALENDAR\r\n"; let href = "/dav/card/u/d/no-uid.vcf"; let jc = vcard_to_jscontact(no_uid, href).expect("synthetic uid"); assert_eq!(jc.uid, synthesise_uid(href)); - assert!(jc.uid.starts_with("vandelay-syn-")); + assert!(jc.uid.starts_with("inbuxa-migrate-syn-")); } #[test] diff --git a/src/sync/import_exchange_ews/coordinator.rs b/src/sync/import_exchange_ews/coordinator.rs index 4ee25cd..dbdc538 100644 --- a/src/sync/import_exchange_ews/coordinator.rs +++ b/src/sync/import_exchange_ews/coordinator.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -385,7 +386,7 @@ fn spawn_token_refresher( let client = client.clone(); let initial_refresh = initial.as_ref().and_then(|t| t.refresh_token.clone()); std::thread::Builder::new() - .name("vandelay-ews-token-refresh".to_owned()) + .name("inbuxa-migrate-ews-token-refresh".to_owned()) .spawn(move || { let mut next_exp = exp_secs; let mut refresh_token = initial_refresh; diff --git a/src/sync/import_exchange_graph/coordinator.rs b/src/sync/import_exchange_graph/coordinator.rs index 6e80238..922141e 100644 --- a/src/sync/import_exchange_graph/coordinator.rs +++ b/src/sync/import_exchange_graph/coordinator.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -395,7 +396,7 @@ fn spawn_token_refresher( let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let shutdown_signal = shutdown.clone(); let handle = std::thread::Builder::new() - .name("vandelay-graph-token-refresh".to_owned()) + .name("inbuxa-migrate-graph-token-refresh".to_owned()) .spawn(move || { while !shutdown_signal.load(std::sync::atomic::Ordering::Relaxed) { let now = unix_now(); diff --git a/src/sync/import_managesieve/coordinator.rs b/src/sync/import_managesieve/coordinator.rs index 71cee22..e7955bd 100644 --- a/src/sync/import_managesieve/coordinator.rs +++ b/src/sync/import_managesieve/coordinator.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -199,7 +200,7 @@ pub fn run(common: CommonConfig, config: ManageSieveImportConfig) -> Result { return Err(Error::Connection(format!( - "ManageSieve referral on GETSCRIPT {name:?}: {e}; vandelay does not follow referrals" + "ManageSieve referral on GETSCRIPT {name:?}: {e}; inbuxa-migrate does not follow referrals" ))); } Disposition::TransportDrop => { @@ -536,7 +537,7 @@ fn listscripts_with_retry( } Disposition::Referral => { return Err(Error::Connection(format!( - "LISTSCRIPTS referral: {e}; vandelay does not follow referrals" + "LISTSCRIPTS referral: {e}; inbuxa-migrate does not follow referrals" ))); } _ => return Err(Error::Connection(format!("LISTSCRIPTS failed: {e}"))), diff --git a/tests/integration/baikal.rs b/tests/integration/baikal.rs index 2ac60dd..ce51ca8 100644 --- a/tests/integration/baikal.rs +++ b/tests/integration/baikal.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -34,7 +35,7 @@ const BAIKAL_YAML: &str = "system: timezone: 'UTC' card_enabled: true cal_enabled: true - invite_from: 'noreply@vandelay.test' + invite_from: 'noreply@inbuxa-migrate.test' dav_auth_type: 'Basic' admin_passwordhash: '004b53ae84286ba6003b34e8ef404826' failed_access_message: 'user %u authentication failure for Baikal' @@ -64,7 +65,7 @@ impl Baikal { pub fn start() -> ContainerResult { let db_bytes = build_sqlite_db()?; - let image: GenericImage = GenericBuildableImage::new("vandelay-baikal", "test") + let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-baikal", "test") .with_dockerfile_string(DOCKERFILE.to_owned()) .with_data(BAIKAL_YAML.as_bytes().to_vec(), "baikal.yaml") .with_data(db_bytes, "db.sqlite") @@ -325,8 +326,8 @@ fn build_sqlite_db() -> ContainerResult> { ) .map_err(|e| ContainerError::Seed(format!("baikal user {name}: {e}")))?; let uri = format!("principals/{name}"); - let email = format!("{name}@vandelay.test"); - let display = format!("Vandelay {name}"); + let email = format!("{name}@inbuxa-migrate.test"); + let display = format!("Inbuxa {name}"); conn.execute( "INSERT INTO principals (uri, email, displayname) VALUES (?1, ?2, ?3)", params![uri, email, display], diff --git a/tests/integration/cyrus.rs b/tests/integration/cyrus.rs index 29d9afa..5a606c2 100644 --- a/tests/integration/cyrus.rs +++ b/tests/integration/cyrus.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -142,7 +143,7 @@ pub struct Cyrus { impl Cyrus { pub fn start() -> ContainerResult { - let image: GenericImage = GenericBuildableImage::new("vandelay-cyrus", "test") + let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-cyrus", "test") .with_dockerfile_string(DOCKERFILE.to_owned()) .with_data(IMAPD_CONF.as_bytes().to_vec(), "imapd.conf") .with_data(CYRUS_CONF.as_bytes().to_vec(), "cyrus.conf") @@ -313,10 +314,13 @@ impl Cyrus { ) -> ContainerResult<(Vec, String)> { let mut client = ImapSeed::connect(&self.imap.host, self.imap.port)?; client.login(&account.username, &account.password)?; - let message_id = format!("", account.username); + let message_id = format!( + "", + account.username + ); let body = format!( - "From: cyrus-added-{tag}@vandelay.test\r\n\ - To: {}@vandelay.test\r\n\ + "From: cyrus-added-{tag}@inbuxa-migrate.test\r\n\ + To: {}@inbuxa-migrate.test\r\n\ Subject: Cyrus added probe {tag}\r\n\ Message-ID: {message_id}\r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ @@ -372,10 +376,10 @@ impl Cyrus { } pub fn flag_probe_message() -> Vec { - let body = "From: probe-flags@vandelay.test\r\n\ - To: user@vandelay.test\r\n\ + let body = "From: probe-flags@inbuxa-migrate.test\r\n\ + To: user@inbuxa-migrate.test\r\n\ Subject: Cyrus flag probe\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ \r\n\ Cyrus flag probe body.\r\n"; diff --git a/tests/integration/data.rs b/tests/integration/data.rs index d34a9f8..9189c94 100644 --- a/tests/integration/data.rs +++ b/tests/integration/data.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -89,15 +90,15 @@ fn synth_messages(n: usize) -> Vec { let mut out = Vec::with_capacity(n); for i in 0..n { let body = format!( - "From: sender{i}@vandelay.test\r\n\ - To: user@vandelay.test\r\n\ + "From: sender{i}@inbuxa-migrate.test\r\n\ + To: user@inbuxa-migrate.test\r\n\ Subject: Synthetic test message {i}\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ MIME-Version: 1.0\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ \r\n\ - This is synthetic seed message #{i} for vandelay container tests.\r\n", + This is synthetic seed message #{i} for inbuxa-migrate container tests.\r\n", ); out.push(MboxMessage { raw: body.into_bytes(), @@ -135,10 +136,10 @@ fn vcard_v3_basic(i: usize) -> String { format!( "BEGIN:VCARD\r\n\ VERSION:3.0\r\n\ - UID:vandelay-card-basic-{i}\r\n\ + UID:inbuxa-migrate-card-basic-{i}\r\n\ FN:Basic Contact {i}\r\n\ N:Contact{i};Basic;;;\r\n\ - EMAIL;TYPE=INTERNET:basic-{i}@vandelay.test\r\n\ + EMAIL;TYPE=INTERNET:basic-{i}@inbuxa-migrate.test\r\n\ END:VCARD\r\n" ) } @@ -147,14 +148,14 @@ fn vcard_v3_full(i: usize) -> String { format!( "BEGIN:VCARD\r\n\ VERSION:3.0\r\n\ - UID:vandelay-card-full-{i}\r\n\ + UID:inbuxa-migrate-card-full-{i}\r\n\ FN:Full Contact {i}\r\n\ N:Contact{i};Full;Middle;;\r\n\ - EMAIL;TYPE=INTERNET;TYPE=WORK:full-{i}@vandelay.test\r\n\ + EMAIL;TYPE=INTERNET;TYPE=WORK:full-{i}@inbuxa-migrate.test\r\n\ TEL;TYPE=CELL:+15550010{i:03}\r\n\ - ORG:Vandelay Test\r\n\ + ORG:Inbuxa Test\r\n\ TITLE:Senior Test Engineer\r\n\ - URL:https://vandelay.test/{i}\r\n\ + URL:https://inbuxa-migrate.test/{i}\r\n\ NOTE:Synthetic full v3 contact #{i}\r\n\ END:VCARD\r\n" ) @@ -164,11 +165,11 @@ fn vcard_v3_multi_email(i: usize) -> String { format!( "BEGIN:VCARD\r\n\ VERSION:3.0\r\n\ - UID:vandelay-card-multi-{i}\r\n\ + UID:inbuxa-migrate-card-multi-{i}\r\n\ FN:Multi Email {i}\r\n\ N:Contact{i};MultiEmail;;;\r\n\ - EMAIL;TYPE=INTERNET;TYPE=HOME:multi-{i}-home@vandelay.test\r\n\ - EMAIL;TYPE=INTERNET;TYPE=WORK:multi-{i}-work@vandelay.test\r\n\ + EMAIL;TYPE=INTERNET;TYPE=HOME:multi-{i}-home@inbuxa-migrate.test\r\n\ + EMAIL;TYPE=INTERNET;TYPE=WORK:multi-{i}-work@inbuxa-migrate.test\r\n\ END:VCARD\r\n" ) } @@ -177,12 +178,12 @@ fn vcard_v3_org_and_title(i: usize) -> String { format!( "BEGIN:VCARD\r\n\ VERSION:3.0\r\n\ - UID:vandelay-card-org-{i}\r\n\ + UID:inbuxa-migrate-card-org-{i}\r\n\ FN:Org Contact {i}\r\n\ N:Contact{i};Org;;Dr.;PhD\r\n\ - ORG:Vandelay Industries;Engineering;Test Group\r\n\ + ORG:Inbuxa Industries;Engineering;Test Group\r\n\ TITLE:Principal Researcher\r\n\ - EMAIL;TYPE=INTERNET:org-{i}@vandelay.test\r\n\ + EMAIL;TYPE=INTERNET:org-{i}@inbuxa-migrate.test\r\n\ END:VCARD\r\n" ) } @@ -194,7 +195,7 @@ fn vcard_v4_basic(i: usize) -> String { UID:urn:uuid:00000000-0000-4000-a000-0000{i:08x}\r\n\ FN:V4 Basic {i}\r\n\ N:Contact{i};V4Basic;;;\r\n\ - EMAIL:v4-basic-{i}@vandelay.test\r\n\ + EMAIL:v4-basic-{i}@inbuxa-migrate.test\r\n\ END:VCARD\r\n" ) } @@ -206,7 +207,7 @@ fn vcard_v4_with_address(i: usize) -> String { UID:urn:uuid:00000000-0000-4000-b000-0000{i:08x}\r\n\ FN:V4 Address {i}\r\n\ N:Contact{i};V4Address;;;\r\n\ - EMAIL:v4-address-{i}@vandelay.test\r\n\ + EMAIL:v4-address-{i}@inbuxa-migrate.test\r\n\ ADR;TYPE=home:;;{i} Test Street;Springfield;OR;97477;US\r\n\ END:VCARD\r\n" ) @@ -220,7 +221,7 @@ fn vcard_v4_nickname(i: usize) -> String { FN:V4 Nick {i}\r\n\ N:Contact{i};V4Nick;;;\r\n\ NICKNAME:Nicky{i}\r\n\ - EMAIL:v4-nick-{i}@vandelay.test\r\n\ + EMAIL:v4-nick-{i}@inbuxa-migrate.test\r\n\ END:VCARD\r\n" ) } @@ -236,7 +237,7 @@ fn vcard_v4_birthday(i: usize) -> String { FN:V4 Birthday {i}\r\n\ N:Contact{i};V4Birthday;;;\r\n\ BDAY:{year:04}{month:02}{day:02}\r\n\ - EMAIL:v4-bday-{i}@vandelay.test\r\n\ + EMAIL:v4-bday-{i}@inbuxa-migrate.test\r\n\ END:VCARD\r\n" ) } @@ -245,10 +246,10 @@ fn vcard_v3_apple_item_labels(i: usize) -> String { format!( "BEGIN:VCARD\r\n\ VERSION:3.0\r\n\ - UID:vandelay-card-ablabels-{i}\r\n\ + UID:inbuxa-migrate-card-ablabels-{i}\r\n\ FN:Apple Labels {i}\r\n\ N:Contact{i};AppleLabels;;;\r\n\ - EMAIL;TYPE=INTERNET:ablabels-{i}@vandelay.test\r\n\ + EMAIL;TYPE=INTERNET:ablabels-{i}@inbuxa-migrate.test\r\n\ ITEM1.X-ABLABEL:Name1\r\n\ ITEM2.X-ABLABEL:Name2\r\n\ ITEM1.X-ABDATE:20171111\r\n\ @@ -287,9 +288,9 @@ fn ical_simple_dt(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-simple-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-simple-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T{hour:02}0000Z\r\n\ DTEND:2025{month:02}{day:02}T{hour:02}3000Z\r\n\ @@ -307,9 +308,9 @@ fn ical_all_day(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-allday-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-allday-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART;VALUE=DATE:2025{month:02}{day:02}\r\n\ DTEND;VALUE=DATE:2025{month:02}{next_day:02}\r\n\ @@ -325,9 +326,9 @@ fn ical_multi_day_dt(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-multiday-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-multiday-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T090000Z\r\n\ DTEND:2025{month:02}{day:02}T170000Z\r\n\ @@ -344,16 +345,16 @@ fn ical_with_organizer_attendees(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-meeting-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-meeting-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T140000Z\r\n\ DTEND:2025{month:02}{day:02}T150000Z\r\n\ SUMMARY:Sync meeting {i}\r\n\ - ORGANIZER;CN=Organiser:mailto:organiser-{i}@vandelay.test\r\n\ - ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED:mailto:alice-{i}@vandelay.test\r\n\ - ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob-{i}@vandelay.test\r\n\ + ORGANIZER;CN=Organiser:mailto:organiser-{i}@inbuxa-migrate.test\r\n\ + ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED:mailto:alice-{i}@inbuxa-migrate.test\r\n\ + ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob-{i}@inbuxa-migrate.test\r\n\ END:VEVENT\r\n\ END:VCALENDAR\r\n" ) @@ -365,15 +366,15 @@ fn ical_with_categories_location(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-cat-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-cat-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T100000Z\r\n\ DTEND:2025{month:02}{day:02}T110000Z\r\n\ SUMMARY:Categorised event {i}\r\n\ LOCATION:Test Lab\\, Room {i}\r\n\ - CATEGORIES:VANDELAY,TEST,FIXTURE\r\n\ + CATEGORIES:INBUXA,TEST,FIXTURE\r\n\ END:VEVENT\r\n\ END:VCALENDAR\r\n" ) @@ -385,9 +386,9 @@ fn ical_recurring_daily(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-daily-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-daily-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T070000Z\r\n\ DTEND:2025{month:02}{day:02}T073000Z\r\n\ @@ -405,9 +406,9 @@ fn ical_recurring_weekly_until(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-weekly-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-weekly-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T160000Z\r\n\ DTEND:2025{month:02}{day:02}T170000Z\r\n\ @@ -424,9 +425,9 @@ fn ical_with_alarm(i: usize) -> String { format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//container-test//EN\r\n\ + PRODID:-//inbuxa-migrate//container-test//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:vandelay-alarm-{i}@vandelay.test\r\n\ + UID:inbuxa-migrate-alarm-{i}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:2025{month:02}{day:02}T080000Z\r\n\ DTEND:2025{month:02}{day:02}T083000Z\r\n\ @@ -445,9 +446,9 @@ pub fn malformed_ical(name: &str) -> RawFixture { let body = format!( "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ - PRODID:-//vandelay//broken//EN\r\n\ + PRODID:-//inbuxa-migrate//broken//EN\r\n\ BEGIN:VEVENT\r\n\ - UID:broken-{name}@vandelay.test\r\n\ + UID:broken-{name}@inbuxa-migrate.test\r\n\ DTSTAMP:20250101T120000Z\r\n\ DTSTART:NOT-A-DATE\r\n\ RRULE:FREQ=BOGUS;INTERVAL=oops\r\n\ diff --git a/tests/integration/dovecot.rs b/tests/integration/dovecot.rs index ccb46ee..3560c10 100644 --- a/tests/integration/dovecot.rs +++ b/tests/integration/dovecot.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -126,7 +127,7 @@ pub struct Dovecot { impl Dovecot { pub fn start() -> ContainerResult { - let image: GenericImage = GenericBuildableImage::new("vandelay-dovecot", "test") + let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-dovecot", "test") .with_dockerfile_string(DOCKERFILE.to_owned()) .with_data(DOVECOT_CONF.as_bytes().to_vec(), "dovecot.conf") .with_data(users_file().into_bytes(), "users") @@ -374,10 +375,10 @@ impl Dovecot { ) -> ContainerResult<(Vec, String)> { let mut client = ImapSeed::connect(&self.imap.host, self.imap.port)?; client.login(&account.username, &account.password)?; - let message_id = format!("", account.username); + let message_id = format!("", account.username); let body = format!( - "From: added-{tag}@vandelay.test\r\n\ - To: {}@vandelay.test\r\n\ + "From: added-{tag}@inbuxa-migrate.test\r\n\ + To: {}@inbuxa-migrate.test\r\n\ Subject: Added probe {tag}\r\n\ Message-ID: {message_id}\r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ @@ -446,10 +447,10 @@ fn histogram_total(h: &HashMap) -> usize { } pub fn flag_probe_message() -> Vec { - let body = "From: probe-flags@vandelay.test\r\n\ - To: user@vandelay.test\r\n\ + let body = "From: probe-flags@inbuxa-migrate.test\r\n\ + To: user@inbuxa-migrate.test\r\n\ Subject: Flag probe\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ \r\n\ Flag probe body.\r\n"; @@ -457,8 +458,8 @@ pub fn flag_probe_message() -> Vec { } pub fn no_message_id_probe() -> Vec { - let body = "From: nomid@vandelay.test\r\n\ - To: user@vandelay.test\r\n\ + let body = "From: nomid@inbuxa-migrate.test\r\n\ + To: user@inbuxa-migrate.test\r\n\ Subject: No Message-ID probe\r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ \r\n\ @@ -467,10 +468,10 @@ pub fn no_message_id_probe() -> Vec { } pub fn shared_message_id_probe() -> Vec { - let body = "From: shared@vandelay.test\r\n\ - To: user@vandelay.test\r\n\ + let body = "From: shared@inbuxa-migrate.test\r\n\ + To: user@inbuxa-migrate.test\r\n\ Subject: Shared MID probe\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ \r\n\ Shared MID body.\r\n"; diff --git a/tests/integration/layouts.rs b/tests/integration/layouts.rs index 688d41b..5c6e75f 100644 --- a/tests/integration/layouts.rs +++ b/tests/integration/layouts.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -63,7 +64,7 @@ pub const ACCOUNT1: &str = "user1"; pub const ACCOUNT2: &str = "user2"; pub const ACCOUNT3: &str = "user3"; -pub const PASSWORD: &str = "VandelayUser#2026"; +pub const PASSWORD: &str = "InbuxaUser#2026"; pub static MAILBOXES_DEEP: &[MailboxSpec] = &[ mailbox!("proj", "Projects", None), diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 05c77f1..4bc821c 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -22,7 +23,7 @@ pub mod webdav; pub use error::{ContainerError, ContainerResult}; -pub const OWNER_LABEL: &str = "art.stalw.vandelay.itest"; +pub const OWNER_LABEL: &str = "art.stalw.inbuxa-migrate.itest"; use std::sync::Once; diff --git a/tests/integration/validate.rs b/tests/integration/validate.rs index 1de4917..9d8bb1a 100644 --- a/tests/integration/validate.rs +++ b/tests/integration/validate.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -9,13 +10,13 @@ use std::path::{Path, PathBuf}; use rusqlite::Connection; -use vandelay::logging::Logger; -use vandelay::sync::CommonConfig; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::CommonConfig; pub fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-container-{tag}-{}-{}.sqlite", + "inbuxa-migrate-container-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/tests/integration/webdav.rs b/tests/integration/webdav.rs index 15d74c7..eece088 100644 --- a/tests/integration/webdav.rs +++ b/tests/integration/webdav.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -137,7 +138,7 @@ pub struct WebDav { impl WebDav { pub fn start() -> ContainerResult { - let image: GenericImage = GenericBuildableImage::new("vandelay-webdav", "test") + let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-webdav", "test") .with_dockerfile_string(DOCKERFILE.to_owned()) .with_data(APACHE_CONF.as_bytes().to_vec(), "apache.conf") .with_data(htpasswd_for_test().into_bytes(), "htpasswd") diff --git a/tests/integration_baikal.rs b/tests/integration_baikal.rs index 1cc929f..3452c6f 100644 --- a/tests/integration_baikal.rs +++ b/tests/integration_baikal.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -16,11 +17,11 @@ use integration::validate::{ contact_card_count, count, open_archive, tmp_archive, }; +use inbuxa_migrate::error::Error; +use inbuxa_migrate::sync::import_dav; +use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; use rusqlite::Connection; use serde_json::Value; -use vandelay::error::Error; -use vandelay::sync::import_dav; -use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; fn dav_config(kind: DavKindArg, account: &Account, dav_root: &str) -> DavImportConfig { let path = match kind { diff --git a/tests/integration_cyrus.rs b/tests/integration_cyrus.rs index 6475d0d..2f78a79 100644 --- a/tests/integration_cyrus.rs +++ b/tests/integration_cyrus.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -16,9 +17,9 @@ use integration::validate::{ }; use integration::{Account, Endpoint}; -use vandelay::error::Error; -use vandelay::sync::import_imap; -use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig}; +use inbuxa_migrate::error::Error; +use inbuxa_migrate::sync::import_imap; +use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig}; fn imap_config(account: &Account, imap: &Endpoint) -> ImapImportConfig { ImapImportConfig { @@ -327,10 +328,10 @@ fn cyrus_non_ascii_mailbox_names_round_trip() { let mutf7_name = "Envoy&AOk-s"; let utf8_name = "Envoyés"; - let message = b"From: accents@vandelay.test\r\n\ - To: user1@vandelay.test\r\n\ + let message = b"From: accents@inbuxa-migrate.test\r\n\ + To: user1@inbuxa-migrate.test\r\n\ Subject: Accents\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ \r\n\ Accented folder probe.\r\n"; diff --git a/tests/integration_dovecot.rs b/tests/integration_dovecot.rs index f99e8ba..e6dcb13 100644 --- a/tests/integration_dovecot.rs +++ b/tests/integration_dovecot.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -16,14 +17,14 @@ use integration::validate::{ mailbox_id_by_path, mailbox_path, open_archive, tmp_archive, }; +use inbuxa_migrate::error::Error; +use inbuxa_migrate::imap::client::{ConnectMode, ImapClient}; +use inbuxa_migrate::imap::transport::Connector; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig}; +use inbuxa_migrate::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig}; +use inbuxa_migrate::sync::{import_imap, import_managesieve}; use rusqlite::Connection; -use vandelay::error::Error; -use vandelay::imap::client::{ConnectMode, ImapClient}; -use vandelay::imap::transport::Connector; -use vandelay::logging::Logger; -use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig}; -use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig}; -use vandelay::sync::{import_imap, import_managesieve}; fn imap_config(account: &Account, imap: &integration::Endpoint) -> ImapImportConfig { ImapImportConfig { @@ -526,10 +527,10 @@ fn dovecot_non_ascii_mailbox_names_round_trip() { let mutf7_name = "Envoy&AOk-s"; let utf8_name = "Envoyés"; - let message = b"From: accents@vandelay.test\r\n\ - To: user1@vandelay.test\r\n\ + let message = b"From: accents@inbuxa-migrate.test\r\n\ + To: user1@inbuxa-migrate.test\r\n\ Subject: Accents\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\ \r\n\ Accented folder probe.\r\n"; diff --git a/tests/integration_radicale.rs b/tests/integration_radicale.rs index 97bc139..c600f9f 100644 --- a/tests/integration_radicale.rs +++ b/tests/integration_radicale.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -16,11 +17,11 @@ use integration::validate::{ }; use integration::{Account, Endpoint}; +use inbuxa_migrate::error::Error; +use inbuxa_migrate::sync::import_dav; +use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; use rusqlite::Connection; use serde_json::Value; -use vandelay::error::Error; -use vandelay::sync::import_dav; -use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; fn dav_config(kind: DavKindArg, account: &Account, http: &Endpoint) -> DavImportConfig { DavImportConfig { diff --git a/tests/integration_webdav.rs b/tests/integration_webdav.rs index dd4a0f5..23e1594 100644 --- a/tests/integration_webdav.rs +++ b/tests/integration_webdav.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -15,9 +16,9 @@ use integration::validate::{ use integration::webdav::{AccountSeed, WebDav}; use integration::{Account, Endpoint}; -use vandelay::error::Error; -use vandelay::sync::import_dav; -use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; +use inbuxa_migrate::error::Error; +use inbuxa_migrate::sync::import_dav; +use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; fn webdav_config(account: &Account, http: &Endpoint) -> DavImportConfig { DavImportConfig { @@ -319,7 +320,7 @@ fn webdav_starts_seeds_and_imports() { drop(conn); let new_payload = format!( - "added-{} content for vandelay webdav add-after-import probe\n", + "added-{} content for inbuxa-migrate webdav add-after-import probe\n", seed.username ); let new_payload_bytes = new_payload.as_bytes().to_vec(); diff --git a/tests/live_stalwart.rs b/tests/live_stalwart.rs index d990d91..ccf8977 100644 --- a/tests/live_stalwart.rs +++ b/tests/live_stalwart.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -7,10 +8,10 @@ mod integration; mod seeder; +use inbuxa_migrate::jmap::account::{self, AccountSelector}; +use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy}; +use inbuxa_migrate::jmap::session::Session; use integration::stalwart::shared as shared_stalwart; -use vandelay::jmap::account::{self, AccountSelector}; -use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy}; -use vandelay::jmap::session::Session; fn admin_client() -> HttpClient { HttpClient::new( diff --git a/tests/mock_dav.rs b/tests/mock_dav.rs index 54cf59c..5acc719 100644 --- a/tests/mock_dav.rs +++ b/tests/mock_dav.rs @@ -1,17 +1,18 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use std::io::Cursor; -use vandelay::dav::client::DavClient; -use vandelay::dav::discover::{DavKind, discover}; -use vandelay::dav::parse::{parse_multistatus, strip_ascii_control_chars}; -use vandelay::dav::xml; -use vandelay::jmap::error::JmapError; -use vandelay::jmap::http::{Auth, RetryPolicy}; +use inbuxa_migrate::dav::client::DavClient; +use inbuxa_migrate::dav::discover::{DavKind, discover}; +use inbuxa_migrate::dav::parse::{parse_multistatus, strip_ascii_control_chars}; +use inbuxa_migrate::dav::xml; +use inbuxa_migrate::jmap::error::JmapError; +use inbuxa_migrate::jmap::http::{Auth, RetryPolicy}; fn client(retries: u32) -> DavClient { DavClient::new( @@ -75,7 +76,7 @@ fn discovery_resolves_per_user_principal_even_when_url_lists_collections() { r#" - {url}/dav/cal/secondary@vandelay.org/default/ + {url}/dav/cal/secondary@inbuxa-migrate.org/default/ @@ -101,7 +102,7 @@ fn discovery_resolves_per_user_principal_even_when_url_lists_collections() { {url}/dav/cal/ - {url}/dav/principals/secondary@vandelay.org/ + {url}/dav/principals/secondary@inbuxa-migrate.org/ HTTP/1.1 200 OK @@ -121,7 +122,7 @@ fn discovery_resolves_per_user_principal_even_when_url_lists_collections() { assert_eq!(disc.collections.len(), 1); assert_eq!( disc.principal_url.as_deref(), - Some(format!("{url}/dav/principals/secondary@vandelay.org/").as_str()), + Some(format!("{url}/dav/principals/secondary@inbuxa-migrate.org/").as_str()), "account identity must be the per-user principal, not the shared base DAV root" ); } @@ -212,7 +213,7 @@ fn discovery_returns_not_found_when_no_collections() { let err = discover(&c, DavKind::Caldav, &server.url()).unwrap_err(); assert!(matches!( err, - vandelay::dav::discover::DiscoveryError::NotFound { .. } + inbuxa_migrate::dav::discover::DiscoveryError::NotFound { .. } )); } @@ -489,14 +490,14 @@ fn webdav_listing_returns_collection_and_file() { #[test] fn google_usage_limits_403_classified_retryable() { - use vandelay::dav::retry::{DavOutcome, classify}; + use inbuxa_migrate::dav::retry::{DavOutcome, classify}; let body = br#"{"error":{"errors":[{"domain":"usageLimits"}]}}"#; assert_eq!(classify(403, body), DavOutcome::Retryable); } #[test] fn non_quota_403_is_auth() { - use vandelay::dav::retry::{DavOutcome, classify}; + use inbuxa_migrate::dav::retry::{DavOutcome, classify}; assert_eq!(classify(403, b"forbidden"), DavOutcome::Auth); } @@ -801,7 +802,7 @@ fn webdav_cycle_breaks_on_lex_smallest() { #[test] fn multiget_405_method_not_allowed_triggers_get_fallback() { - use vandelay::dav::retry::{DavOutcome, classify}; + use inbuxa_migrate::dav::retry::{DavOutcome, classify}; assert_eq!(classify(405, b""), DavOutcome::Fatal); } @@ -893,8 +894,8 @@ fn streaming_propfind_404_returns_empty_responses() { #[test] fn source_change_protection_rejects_different_account_id() { + use inbuxa_migrate::db::{init, sources}; use rusqlite::Connection; - use vandelay::db::{init, sources}; let conn = Connection::open_in_memory().unwrap(); init::apply_schema(&conn).unwrap(); @@ -1049,15 +1050,15 @@ fn webdav_discovery_keeps_only_self_row_as_root() { #[test] fn webdav_root_collection_is_not_materialised_children_map_to_target_root() { + use inbuxa_migrate::dav::discover::DiscoveredCollection; + use inbuxa_migrate::dav::href::Href; + use inbuxa_migrate::dav::parse::ResourceProps; + use inbuxa_migrate::db; + use inbuxa_migrate::db::sources::SourceKey; + use inbuxa_migrate::logging::Logger; + use inbuxa_migrate::sync::TypeCounts; + use inbuxa_migrate::sync::import_dav::tree::{WebDavCtx, reconcile_filenodes}; use rusqlite::Connection; - use vandelay::dav::discover::DiscoveredCollection; - use vandelay::dav::href::Href; - use vandelay::dav::parse::ResourceProps; - use vandelay::db; - use vandelay::db::sources::SourceKey; - use vandelay::logging::Logger; - use vandelay::sync::TypeCounts; - use vandelay::sync::import_dav::tree::{WebDavCtx, reconcile_filenodes}; let mut server = mockito::Server::new(); let url = server.url(); @@ -1336,10 +1337,10 @@ fn webdav_display_or_basename_test_via_parser_keeps_basename_semantics() { #[test] fn dry_run_writes_nothing_but_emits_per_collection_counts() { + use inbuxa_migrate::logging::Logger; + use inbuxa_migrate::sync::CommonConfig; + use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run}; use std::path::PathBuf; - use vandelay::logging::Logger; - use vandelay::sync::CommonConfig; - use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run}; let mut server = mockito::Server::new(); let url = server.url(); @@ -1381,7 +1382,7 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() { let _items = multistatus_response(&mut server, "PROPFIND", "/dav/cal/u/default/", &items_body); let archive: PathBuf = std::env::temp_dir().join(format!( - "vandelay-dryrun-{}-{}.sqlite", + "inbuxa-migrate-dryrun-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1449,10 +1450,10 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() { #[test] fn dav_source_change_protection_fires_across_users_on_same_root() { use encodify::base64::STANDARD; + use inbuxa_migrate::logging::Logger; + use inbuxa_migrate::sync::CommonConfig; + use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run}; use std::path::PathBuf; - use vandelay::logging::Logger; - use vandelay::sync::CommonConfig; - use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run}; let mut server = mockito::Server::new(); let url = server.url(); @@ -1499,8 +1500,14 @@ fn dav_source_change_protection_fires_across_users_on_same_root() { .with_body(&empty_items) .create(); - let auth_a = format!("Basic {}", STANDARD.encode("secondary@vandelay.org:passA")); - let auth_b = format!("Basic {}", STANDARD.encode("tertiary@vandelay.org:passB")); + let auth_a = format!( + "Basic {}", + STANDARD.encode("secondary@inbuxa-migrate.org:passA") + ); + let auth_b = format!( + "Basic {}", + STANDARD.encode("tertiary@inbuxa-migrate.org:passB") + ); let principal = |who: &str| { format!( r#" @@ -1521,7 +1528,7 @@ fn dav_source_change_protection_fires_across_users_on_same_root() { .match_header("authorization", auth_a.as_str()) .with_status(207) .with_header("content-type", "application/xml; charset=utf-8") - .with_body(principal("secondary@vandelay.org")) + .with_body(principal("secondary@inbuxa-migrate.org")) .create(); let _pb = server .mock("PROPFIND", "/dav/cal/") @@ -1529,11 +1536,11 @@ fn dav_source_change_protection_fires_across_users_on_same_root() { .match_header("authorization", auth_b.as_str()) .with_status(207) .with_header("content-type", "application/xml; charset=utf-8") - .with_body(principal("tertiary@vandelay.org")) + .with_body(principal("tertiary@inbuxa-migrate.org")) .create(); let archive: PathBuf = std::env::temp_dir().join(format!( - "vandelay-dav-srcchange-{}-{}.sqlite", + "inbuxa-migrate-dav-srcchange-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1563,19 +1570,27 @@ fn dav_source_change_protection_fires_across_users_on_same_root() { allow_source_change: false, }; - run(common(&archive), config("secondary@vandelay.org", "passA")).expect("user A import ok"); - let err = run(common(&archive), config("tertiary@vandelay.org", "passB")).unwrap_err(); + run( + common(&archive), + config("secondary@inbuxa-migrate.org", "passA"), + ) + .expect("user A import ok"); + let err = run( + common(&archive), + config("tertiary@inbuxa-migrate.org", "passB"), + ) + .unwrap_err(); let _ = std::fs::remove_file(&archive); assert!( - matches!(err, vandelay::error::Error::SourceChange(_)), + matches!(err, inbuxa_migrate::error::Error::SourceChange(_)), "importing a different user into the same archive must trigger source-change protection; got {err:?}" ); } #[test] fn source_change_protection_rejects_different_session_url_same_account() { + use inbuxa_migrate::db::{init, sources}; use rusqlite::Connection; - use vandelay::db::{init, sources}; let conn = Connection::open_in_memory().unwrap(); init::apply_schema(&conn).unwrap(); @@ -1627,8 +1642,8 @@ fn etag_missing_in_enumeration_propagates_to_storage_layer() { #[test] fn mixed_source_archive_allows_distinct_kinds_against_same_url() { + use inbuxa_migrate::db::{init, sources}; use rusqlite::Connection; - use vandelay::db::{init, sources}; let conn = Connection::open_in_memory().unwrap(); init::apply_schema(&conn).unwrap(); @@ -1663,7 +1678,7 @@ fn dav_archive() -> std::path::PathBuf { let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-mockdav-{}-{:?}-{n}.sqlite", + "inbuxa-migrate-mockdav-{}-{:?}-{n}.sqlite", std::process::id(), std::thread::current().id(), )); @@ -1671,11 +1686,11 @@ fn dav_archive() -> std::path::PathBuf { p } -fn caldav_import_config(url: &str) -> vandelay::sync::import_dav::DavImportConfig { - vandelay::sync::import_dav::DavImportConfig { - kind: vandelay::sync::import_dav::DavKindArg::Caldav, +fn caldav_import_config(url: &str) -> inbuxa_migrate::sync::import_dav::DavImportConfig { + inbuxa_migrate::sync::import_dav::DavImportConfig { + kind: inbuxa_migrate::sync::import_dav::DavKindArg::Caldav, url: url.to_owned(), - auth: vandelay::sync::import_dav::DavAuth::Basic { + auth: inbuxa_migrate::sync::import_dav::DavAuth::Basic { user: "u".to_owned(), password: "p".to_owned(), }, @@ -1686,14 +1701,14 @@ fn caldav_import_config(url: &str) -> vandelay::sync::import_dav::DavImportConfi } } -fn dav_common(archive: &std::path::Path) -> vandelay::sync::CommonConfig { - vandelay::sync::CommonConfig { +fn dav_common(archive: &std::path::Path) -> inbuxa_migrate::sync::CommonConfig { + inbuxa_migrate::sync::CommonConfig { archive: archive.to_path_buf(), threads: 1, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::from_flags(true, 0), + logger: inbuxa_migrate::logging::Logger::from_flags(true, 0), } } @@ -1788,7 +1803,7 @@ END:VCALENDAR .create(); let archive = dav_archive(); - let summary = vandelay::sync::import_dav::run( + let summary = inbuxa_migrate::sync::import_dav::run( dav_common(&archive), caldav_import_config(&format!("{url}/dav/cal/u/")), ) @@ -1828,14 +1843,14 @@ fn credentials_rejected_during_discovery_still_aborts_the_run() { .create(); let archive = dav_archive(); - let err = vandelay::sync::import_dav::run( + let err = inbuxa_migrate::sync::import_dav::run( dav_common(&archive), caldav_import_config(&format!("{url}/dav/cal/u/")), ) .expect_err("credentials rejected for the principal aborts the run"); assert!( - matches!(err, vandelay::error::Error::Connection(_)), + matches!(err, inbuxa_migrate::error::Error::Connection(_)), "discovery-level auth rejection is a whole-run failure, got {err:?}" ); assert_eq!(err.exit_code(), 2); @@ -1871,7 +1886,7 @@ fn dav_import_reports_both_phases_when_every_collection_fails() { .create(); let archive = dav_archive(); - let outcome = vandelay::sync::import_dav::run_reporting( + let outcome = inbuxa_migrate::sync::import_dav::run_reporting( dav_common(&archive), caldav_import_config(&format!("{url}/dav/cal/u/")), ); diff --git a/tests/mock_exchange_ews.rs b/tests/mock_exchange_ews.rs index f4dd7ac..539974e 100644 --- a/tests/mock_exchange_ews.rs +++ b/tests/mock_exchange_ews.rs @@ -1,25 +1,28 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use encodify::base64::{LENIENT, STANDARD}; -use mockito::Matcher; -use vandelay::exchange_ews::EwsClient; -use vandelay::exchange_ews::autodiscover::{DiscoverySource, discover}; -use vandelay::exchange_ews::error::EwsError; -use vandelay::exchange_ews::parse::{ +use inbuxa_migrate::exchange_ews::EwsClient; +use inbuxa_migrate::exchange_ews::autodiscover::{DiscoverySource, discover}; +use inbuxa_migrate::exchange_ews::error::EwsError; +use inbuxa_migrate::exchange_ews::parse::{ EnvelopeKind, parse_find_folder_response, parse_find_item_response, parse_get_attachment_inline, parse_response_messages, parse_sync_folder_items_response, read_envelope_summary, }; -use vandelay::exchange_ews::types::{FolderClass, FolderId, ItemId, ResponseCode, ServerVersion}; -use vandelay::exchange_ews::xml::{ +use inbuxa_migrate::exchange_ews::types::{ + FolderClass, FolderId, ItemId, ResponseCode, ServerVersion, +}; +use inbuxa_migrate::exchange_ews::xml::{ FolderRef, ItemShape, Traversal, find_folder_body, find_item_body, get_attachment_body, get_item_body, sync_folder_items_body, }; -use vandelay::jmap::http::{Auth, RetryPolicy}; +use inbuxa_migrate::jmap::http::{Auth, RetryPolicy}; +use mockito::Matcher; const TXT_XML: &str = "text/xml; charset=utf-8"; const APP_JSON: &str = "application/json"; @@ -59,7 +62,7 @@ fn autodiscover_v2_returns_global_endpoint() { .create(); let _ = server; let url = "https://outlook.office365.com/EWS/Exchange.asmx"; - assert!(vandelay::exchange_ews::autodiscover::is_fully_qualified_ews_url(url)); + assert!(inbuxa_migrate::exchange_ews::autodiscover::is_fully_qualified_ews_url(url)); let r = discover(Some(url), None, None, false).unwrap(); assert_eq!(r.source, DiscoverySource::SuppliedUrl); assert_eq!(r.ews_url, url); @@ -265,7 +268,7 @@ fn server_busy_with_back_off_triggers_retry_and_eventually_succeeds() { let c = client(3); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -290,7 +293,7 @@ fn http_401_surfaces_as_auth_error() { let c = client(0); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -313,7 +316,7 @@ fn mime_content_round_trips_through_base64_decode() { " )); let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap(); - let item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap(); + let item = inbuxa_migrate::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap(); let s = item.mime_content.unwrap(); let bytes = LENIENT.decode(&s).unwrap(); assert_eq!(bytes, original); @@ -341,7 +344,7 @@ fn get_attachment_inline_decodes_photo_blob() { #[test] fn calendar_item_master_inlines_modified_and_deleted_occurrences() { - let body = "\ + let body = "\ \ \ Daily\ @@ -359,8 +362,8 @@ fn calendar_item_master_inlines_modified_and_deleted_occurrences() { \ 2025-06-17T14:00:00Z\ \ - "; - let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap(); + "; + let item = inbuxa_migrate::exchange_ews::parse::parse_calendar_item(body).unwrap(); assert_eq!(item.id.id, "M1"); assert_eq!(item.uid.as_deref(), Some("uid-1")); assert_eq!(item.modified_occurrences.len(), 1); @@ -373,7 +376,7 @@ fn calendar_item_master_inlines_modified_and_deleted_occurrences() { #[test] fn organizer_and_attendee_addresses_survive_an_ex_routing_type() { - let body = "\ + let body = "\ \ \ Review\ @@ -386,9 +389,9 @@ fn organizer_and_attendee_addresses_survive_an_ex_routing_type() { Kristina Morgental\ /o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=bdc77b18152647a29d28ce1188376dc9-kristina\ EXUnknown\ - "; - let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap(); - let event = vandelay::exchange_ews::calendar_map::to_jscalendar(&item); + "; + let item = inbuxa_migrate::exchange_ews::parse::parse_calendar_item(body).unwrap(); + let event = inbuxa_migrate::exchange_ews::calendar_map::to_jscalendar(&item); assert_eq!( event.data["organizerCalendarAddress"], "mailto:alice@example.com", @@ -409,7 +412,7 @@ fn organizer_and_attendee_addresses_survive_an_ex_routing_type() { attendee["calendarAddress"] .as_str() .unwrap() - .starts_with("urn:x-vandelay:attendee:"), + .starts_with("urn:x-inbuxa-migrate:attendee:"), "a legacy directory reference cannot be resolved and stays synthetic" ); assert!( @@ -420,7 +423,7 @@ fn organizer_and_attendee_addresses_survive_an_ex_routing_type() { #[test] fn recurrence_end_date_with_a_timezone_offset_yields_a_bounded_series() { - let body = "\ + let body = "\ \ \ Biweekly\ @@ -432,9 +435,9 @@ fn recurrence_end_date_with_a_timezone_offset_yields_a_bounded_series() { 2Wednesday\ 2021-08-04-06:002021-09-30-06:00\ \ - "; - let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap(); - let event = vandelay::exchange_ews::calendar_map::to_jscalendar(&item); + "; + let item = inbuxa_migrate::exchange_ews::parse::parse_calendar_item(body).unwrap(); + let event = inbuxa_migrate::exchange_ews::calendar_map::to_jscalendar(&item); let rule = &event.data["recurrenceRule"]; assert_eq!(rule["frequency"], "weekly"); assert_eq!( @@ -486,8 +489,8 @@ fn invalid_sync_state_data_fault_is_surfaced() { #[test] fn coordinator_diff_classifies_new_vanished_changed_unchanged() { - use vandelay::db::exchange_ews_ids::ItemRow; - use vandelay::sync::import_exchange_ews::items::{EnumeratedItem, diff}; + use inbuxa_migrate::db::exchange_ews_ids::ItemRow; + use inbuxa_migrate::sync::import_exchange_ews::items::{EnumeratedItem, diff}; let server = vec![ EnumeratedItem { @@ -532,9 +535,9 @@ fn coordinator_diff_classifies_new_vanished_changed_unchanged() { #[test] fn source_change_protection_refuses_different_account() { + use inbuxa_migrate::db; + use inbuxa_migrate::db::sources::SourceKey; use rusqlite::Connection; - use vandelay::db; - use vandelay::db::sources::SourceKey; let tmp = tempfile::NamedTempFile::new().unwrap(); let conn = Connection::open(tmp.path()).unwrap(); @@ -557,9 +560,9 @@ fn source_change_protection_refuses_different_account() { #[test] fn mailbox_kinds_are_three_separate_sources() { + use inbuxa_migrate::db; + use inbuxa_migrate::db::sources::SourceKey; use rusqlite::Connection; - use vandelay::db; - use vandelay::db::sources::SourceKey; let tmp = tempfile::NamedTempFile::new().unwrap(); let conn = Connection::open(tmp.path()).unwrap(); @@ -653,7 +656,7 @@ fn http_500_with_server_busy_body_is_treated_as_fault_and_retried() { let c = client(3); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -710,7 +713,7 @@ fn invalid_server_version_fault_downgrades_and_succeeds() { assert_eq!(c.server_version(), ServerVersion::Exchange2013Sp1); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -748,7 +751,7 @@ fn unsupported_version_floor_surfaces_as_soap_fault() { let c = client(0); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -801,7 +804,7 @@ fn server_affinity_cookie_is_captured_and_resent() { let c = client(0); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -823,7 +826,7 @@ fn http_456_surfaces_as_account_locked_auth_error() { let c = client(0); let body = find_folder_body( FolderRef::Distinguished( - vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, + inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot, ), Traversal::Deep, ); @@ -836,8 +839,8 @@ fn http_456_surfaces_as_account_locked_auth_error() { #[test] fn for_each_fetched_item_streams_every_id_across_windows() { - use vandelay::logging::Logger; - use vandelay::sync::import_exchange_ews::items::{ItemRunCtx, for_each_fetched_item}; + use inbuxa_migrate::logging::Logger; + use inbuxa_migrate::sync::import_exchange_ews::items::{ItemRunCtx, for_each_fetched_item}; let mut server = mockito::Server::new(); let url = format!("{}/EWS/Exchange.asmx", server.url()); diff --git a/tests/mock_exchange_graph.rs b/tests/mock_exchange_graph.rs index bef2075..51af475 100644 --- a/tests/mock_exchange_graph.rs +++ b/tests/mock_exchange_graph.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -7,21 +8,21 @@ use std::sync::Once; use encodify::base64::URL_SAFE_NO_PAD; -use mockito::{Matcher, Server}; -use serde_json::json; -use vandelay::exchange_graph::api::{Endpoints, collect_all_ids, paged_collect}; -use vandelay::exchange_graph::calendar_map::{EventType, classify_event_type, convert_event}; -use vandelay::exchange_graph::client::{Accept, GraphClient}; -use vandelay::exchange_graph::contact_map::convert_contact; -use vandelay::exchange_graph::error::GraphError; -use vandelay::exchange_graph::oauth::{ +use inbuxa_migrate::exchange_graph::api::{Endpoints, collect_all_ids, paged_collect}; +use inbuxa_migrate::exchange_graph::calendar_map::{EventType, classify_event_type, convert_event}; +use inbuxa_migrate::exchange_graph::client::{Accept, GraphClient}; +use inbuxa_migrate::exchange_graph::contact_map::convert_contact; +use inbuxa_migrate::exchange_graph::error::GraphError; +use inbuxa_migrate::exchange_graph::oauth::{ DeviceCodeResponse, TokenResponse, parse_device_code_response, parse_token_response, run_device_code_polling_against, }; -use vandelay::exchange_graph::recurrence::convert_patterned_recurrence; -use vandelay::exchange_graph::retry::{HttpClass, classify_http_status}; -use vandelay::exchange_graph::types::Surfaces; -use vandelay::jmap::http::RetryPolicy; +use inbuxa_migrate::exchange_graph::recurrence::convert_patterned_recurrence; +use inbuxa_migrate::exchange_graph::retry::{HttpClass, classify_http_status}; +use inbuxa_migrate::exchange_graph::types::Surfaces; +use inbuxa_migrate::jmap::http::RetryPolicy; +use mockito::{Matcher, Server}; +use serde_json::json; static INIT: Once = Once::new(); @@ -525,7 +526,7 @@ fn contact_minimal_yields_jscontact_card() { "emailAddresses": [{"name": "Alice", "address": "alice@x.com"}] }); let out = convert_contact(&c).unwrap(); - assert!(out.uid.starts_with("vandelay-graph-")); + assert!(out.uid.starts_with("inbuxa-migrate-graph-")); assert_eq!(out.data["@type"], "Card"); assert_eq!(out.data["name"]["full"], "Alice Liddell"); let emails = out.data["emails"].as_object().unwrap(); @@ -569,23 +570,23 @@ fn integration_dry_run_against_mock_server_lists_three_surfaces() { .with_body(r#"{"value":[{"id":"CON1","displayName":"Contacts"}]}"#) .create(); let base = server.url(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: tempfile::NamedTempFile::new().unwrap().path().to_owned(), threads: 2, dry_run: true, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "alice@x.com"), }, api_base: base.clone(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: Surfaces::ALL, - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -593,7 +594,7 @@ fn integration_dry_run_against_mock_server_lists_three_surfaces() { event_attachments: true, allow_source_change: false, }; - let summary = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); + let summary = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); let mailbox = summary .per_type .iter() @@ -681,23 +682,23 @@ fn integration_full_run_mail_only_imports_mime_via_value() { let tmp = tempfile::NamedTempFile::new().unwrap(); let archive_path = tmp.path().to_owned(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive_path.clone(), threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "alice@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -706,7 +707,7 @@ fn integration_full_run_mail_only_imports_mime_via_value() { allow_source_change: false, }; drop(tmp); - let summary = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); + let summary = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); let emails = summary .per_type .iter() @@ -780,23 +781,23 @@ fn integration_duplicate_message_id_does_not_abort_run() { let tmp = tempfile::NamedTempFile::new().unwrap(); let archive_path = tmp.path().to_owned(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive_path.clone(), threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "alice@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -805,7 +806,7 @@ fn integration_duplicate_message_id_does_not_abort_run() { allow_source_change: false, }; drop(tmp); - let summary = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); + let summary = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); let emails = summary .per_type .iter() @@ -880,15 +881,15 @@ fn integration_full_run_is_convergent_on_second_invocation() { let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); let api_base = server.url(); - let make_config = || vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let make_config = || inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "alice@x.com"), }, api_base: api_base.clone(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -896,18 +897,20 @@ fn integration_full_run_is_convergent_on_second_invocation() { event_attachments: true, allow_source_change: false, }; - let make_common = |path: std::path::PathBuf| vandelay::sync::CommonConfig { + let make_common = |path: std::path::PathBuf| inbuxa_migrate::sync::CommonConfig { archive: path, threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let first = - vandelay::sync::import_exchange_graph::run(make_common(archive.clone()), make_config()) - .unwrap(); + let first = inbuxa_migrate::sync::import_exchange_graph::run( + make_common(archive.clone()), + make_config(), + ) + .unwrap(); let created_first = first .per_type .iter() @@ -917,7 +920,8 @@ fn integration_full_run_is_convergent_on_second_invocation() { assert_eq!(created_first, 1); let second = - vandelay::sync::import_exchange_graph::run(make_common(archive), make_config()).unwrap(); + inbuxa_migrate::sync::import_exchange_graph::run(make_common(archive), make_config()) + .unwrap(); let created_second = second .per_type .iter() @@ -930,10 +934,10 @@ fn integration_full_run_is_convergent_on_second_invocation() { #[test] fn source_change_protection_refuses_a_different_account() { let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let conn = vandelay::db::init::open(&archive).unwrap(); - vandelay::db::sources::upsert_source( + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); + inbuxa_migrate::db::sources::upsert_source( &conn, - &vandelay::db::sources::SourceKey { + &inbuxa_migrate::db::sources::SourceKey { kind: "exchange_graph".to_owned(), session_url: "https://login.microsoftonline.com/common|https://graph.microsoft.com/v1.0" @@ -954,23 +958,23 @@ fn source_change_protection_refuses_a_different_account() { .with_body(r#"{"id":"new-user-id","userPrincipalName":"new@x.com"}"#) .create(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive, threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "new@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: Surfaces::ALL, - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -978,9 +982,9 @@ fn source_change_protection_refuses_a_different_account() { event_attachments: true, allow_source_change: false, }; - let err = vandelay::sync::import_exchange_graph::run(common, config).unwrap_err(); + let err = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap_err(); assert!( - matches!(err, vandelay::error::Error::SourceChange(_)), + matches!(err, inbuxa_migrate::error::Error::SourceChange(_)), "expected SourceChange error, got {err:?}" ); } @@ -1011,35 +1015,35 @@ fn stub_well_known_folders(server: &mut Server, inbox_id: &str) { .create(); } -fn make_common(archive: std::path::PathBuf) -> vandelay::sync::CommonConfig { - vandelay::sync::CommonConfig { +fn make_common(archive: std::path::PathBuf) -> inbuxa_migrate::sync::CommonConfig { + inbuxa_migrate::sync::CommonConfig { archive, threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), } } -fn surfaces(list: &str) -> vandelay::exchange_graph::types::Surfaces { - vandelay::exchange_graph::types::Surfaces::parse_list(list).unwrap() +fn surfaces(list: &str) -> inbuxa_migrate::exchange_graph::types::Surfaces { + inbuxa_migrate::exchange_graph::types::Surfaces::parse_list(list).unwrap() } fn make_config( api_base: String, user_target: Option, - surfaces: vandelay::exchange_graph::types::Surfaces, -) -> vandelay::sync::import_exchange_graph::GraphImportConfig { - vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + surfaces: inbuxa_migrate::exchange_graph::types::Surfaces, +) -> inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "alice@x.com"), }, api_base, user_target, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces, - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -1097,15 +1101,15 @@ fn users_upn_routes_through_users_segment_not_me() { .expect_at_least(0) .create(); let base = server.url(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: tempfile::NamedTempFile::new().unwrap().path().to_owned(), threads: 2, dry_run: true, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( common, make_config(base, Some("alice@x.com".to_owned()), Surfaces::ALL), ) @@ -1207,7 +1211,7 @@ fn series_master_with_exception_merges_into_recurrence_overrides() { .create(); let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("calendar")), ) @@ -1220,7 +1224,7 @@ fn series_master_with_exception_merges_into_recurrence_overrides() { .unwrap_or(0); assert_eq!(events, 1, "exactly one master row, exception merged in"); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let row: String = conn .query_row("SELECT data FROM calendar_events", [], |row| row.get(0)) .unwrap(); @@ -1312,7 +1316,7 @@ fn occurrence_event_is_skipped_no_row_no_id_mapping() { .create(); let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("calendar")), ) @@ -1326,7 +1330,7 @@ fn occurrence_event_is_skipped_no_row_no_id_mapping() { assert_eq!(events, 0, "occurrence must not be fetched or stored"); occ_get.assert(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let count: i64 = conn .query_row("SELECT COUNT(*) FROM calendar_events", [], |row| row.get(0)) .unwrap(); @@ -1392,12 +1396,12 @@ fn hidden_mail_folder_has_is_subscribed_zero() { stub_well_known_folders(&mut server, "FVISIBLE"); let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let _ = vandelay::sync::import_exchange_graph::run( + let _ = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("mail")), ) .unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let visible: i64 = conn .query_row( "SELECT is_subscribed FROM mailboxes WHERE name = 'Inbox'", @@ -1486,12 +1490,12 @@ fn well_known_folder_probes_assign_jmap_roles() { } let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let _ = vandelay::sync::import_exchange_graph::run( + let _ = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("mail")), ) .unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let mut stmt = conn .prepare("SELECT name, role FROM mailboxes ORDER BY name") .unwrap(); @@ -1563,7 +1567,7 @@ SGVsbG8=\r\n--X--\r\n"; .create(); let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive), make_config(base, None, surfaces("mail")), ) @@ -1651,7 +1655,7 @@ fn event_get_carries_outlook_timezone_and_body_content_type_prefer() { .create(); let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let _ = vandelay::sync::import_exchange_graph::run( + let _ = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive), make_config(base, None, surfaces("calendar")), ) @@ -1737,23 +1741,23 @@ fn full_run_records_graph_id_in_sync_id_exchange_graph_with_padding() { let tmp = tempfile::NamedTempFile::new().unwrap(); let archive = tmp.path().to_owned(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive.clone(), threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "pad@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -1762,8 +1766,8 @@ fn full_run_records_graph_id_in_sync_id_exchange_graph_with_padding() { allow_source_change: false, }; drop(tmp); - let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let (graph_id, type_name): (String, String) = conn .query_row( "SELECT graph_id, type_name FROM sync_id_exchange_graph", @@ -1810,23 +1814,23 @@ fn archive_mailbox_kind_encodes_synthetic_suffix_in_account_id() { let tmp = tempfile::NamedTempFile::new().unwrap(); let archive = tmp.path().to_owned(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive.clone(), threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "a@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Archive, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Archive, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -1835,8 +1839,8 @@ fn archive_mailbox_kind_encodes_synthetic_suffix_in_account_id() { allow_source_change: false, }; drop(tmp); - let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let account_id: String = conn .query_row( "SELECT account_id FROM sources WHERE kind = 'exchange_graph'", @@ -1850,10 +1854,10 @@ fn archive_mailbox_kind_encodes_synthetic_suffix_in_account_id() { #[test] fn allow_source_change_permits_overwriting_a_different_account() { let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let conn = vandelay::db::init::open(&archive).unwrap(); - vandelay::db::sources::upsert_source( + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); + inbuxa_migrate::db::sources::upsert_source( &conn, - &vandelay::db::sources::SourceKey { + &inbuxa_migrate::db::sources::SourceKey { kind: "exchange_graph".to_owned(), session_url: "https://login.microsoftonline.com/common|https://x.example".to_owned(), account_id: "different-uid".to_owned(), @@ -1878,23 +1882,23 @@ fn allow_source_change_permits_overwriting_a_different_account() { .with_body(r#"{"value":[]}"#) .create(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive.clone(), threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "new@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -1902,7 +1906,7 @@ fn allow_source_change_permits_overwriting_a_different_account() { event_attachments: true, allow_source_change: true, }; - let result = vandelay::sync::import_exchange_graph::run(common, config); + let result = inbuxa_migrate::sync::import_exchange_graph::run(common, config); assert!( result.is_ok(), "--allow-source-change must permit a different account, got {result:?}" @@ -1962,23 +1966,23 @@ fn dry_run_makes_no_per_item_get_and_no_sqlite_writes() { let tmp = tempfile::NamedTempFile::new().unwrap(); let archive = tmp.path().to_owned(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive.clone(), threads: 2, dry_run: true, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "a@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: Surfaces::ALL, - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -1987,10 +1991,10 @@ fn dry_run_makes_no_per_item_get_and_no_sqlite_writes() { allow_source_change: false, }; drop(tmp); - let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); + let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); no_messages.assert(); no_well_known.assert(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let sources: i64 = conn .query_row("SELECT count(*) FROM sources", [], |row| row.get(0)) .unwrap(); @@ -2025,10 +2029,10 @@ fn token_response_bad_code_recognised() { #[test] fn folder_enumeration_failure_skips_vanished_deletion() { let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let setup_conn = vandelay::db::init::open(&archive).unwrap(); - let sid = vandelay::db::sources::upsert_source( + let setup_conn = inbuxa_migrate::db::init::open(&archive).unwrap(); + let sid = inbuxa_migrate::db::sources::upsert_source( &setup_conn, - &vandelay::db::sources::SourceKey { + &inbuxa_migrate::db::sources::SourceKey { kind: "exchange_graph".to_owned(), session_url: "stale".to_owned(), account_id: "stale-uid".to_owned(), @@ -2043,10 +2047,10 @@ fn folder_enumeration_failure_skips_vanished_deletion() { [], ) .unwrap(); - vandelay::db::exchange_graph_ids::insert( + inbuxa_migrate::db::exchange_graph_ids::insert( &setup_conn, sid, - vandelay::db::exchange_graph_ids::MAILBOX, + inbuxa_migrate::db::exchange_graph_ids::MAILBOX, "FMAIL", 42, ) @@ -2067,10 +2071,10 @@ fn folder_enumeration_failure_skips_vanished_deletion() { rusqlite::params![blob_id], ) .unwrap(); - vandelay::db::exchange_graph_ids::insert( + inbuxa_migrate::db::exchange_graph_ids::insert( &setup_conn, sid, - vandelay::db::exchange_graph_ids::EMAIL, + inbuxa_migrate::db::exchange_graph_ids::EMAIL, "MSG-LOCAL", 1, ) @@ -2133,23 +2137,23 @@ fn folder_enumeration_failure_skips_vanished_deletion() { .with_body("transient outage") .create(); - let common = vandelay::sync::CommonConfig { + let common = inbuxa_migrate::sync::CommonConfig { archive: archive.clone(), threads: 2, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::new(0), + logger: inbuxa_migrate::logging::Logger::new(0), }; - let config = vandelay::sync::import_exchange_graph::GraphImportConfig { - auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired { + let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig { + auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired { token: make_jwt(9999999999, "flake@x.com"), }, api_base: server.url(), user_target: None, - mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary, + mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary, surfaces: surfaces("mail"), - event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text, + event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text, graph_connections: 2, top: 100, exception_window_years: 5, @@ -2157,8 +2161,8 @@ fn folder_enumeration_failure_skips_vanished_deletion() { event_attachments: true, allow_source_change: true, }; - let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let remaining: i64 = conn .query_row("SELECT count(*) FROM emails WHERE id = 1", [], |row| { row.get(0) @@ -2324,7 +2328,7 @@ fn contacts_surface_imports_only_address_books_and_cards() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - vandelay::sync::import_exchange_graph::run( + inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("contacts")), ) @@ -2332,7 +2336,7 @@ fn contacts_surface_imports_only_address_books_and_cards() { no_mail.assert(); no_calendars.assert(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); assert_eq!(row_count(&conn, "address_books"), 1); assert_eq!(row_count(&conn, "contact_cards"), 1); assert_eq!(row_count(&conn, "mailboxes"), 0); @@ -2368,7 +2372,7 @@ fn mail_surface_imports_only_mailboxes_and_emails() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - vandelay::sync::import_exchange_graph::run( + inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("mail")), ) @@ -2376,7 +2380,7 @@ fn mail_surface_imports_only_mailboxes_and_emails() { no_calendars.assert(); no_contact_folders.assert(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); assert_eq!(row_count(&conn, "mailboxes"), 1); assert_eq!(row_count(&conn, "emails"), 1); assert_eq!(row_count(&conn, "address_books"), 0); @@ -2442,7 +2446,7 @@ fn default_contact_folder_is_imported_although_contactfolders_omits_it() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("contacts")), ) @@ -2468,7 +2472,7 @@ fn default_contact_folder_is_imported_although_contactfolders_omits_it() { "both default-folder contacts import" ); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let is_default: i64 = conn .query_row("SELECT is_default FROM address_books", [], |row| row.get(0)) .unwrap(); @@ -2518,13 +2522,13 @@ fn default_contact_folder_falls_back_to_parent_of_an_existing_contact() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - vandelay::sync::import_exchange_graph::run( + inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("contacts")), ) .unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let name: String = conn .query_row("SELECT name FROM address_books", [], |row| row.get(0)) .unwrap(); @@ -2568,7 +2572,7 @@ fn contact_folder_reachable_by_two_paths_is_inserted_once() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("contacts")), ) @@ -2638,13 +2642,13 @@ fn message_state_becomes_jmap_keywords() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - vandelay::sync::import_exchange_graph::run( + inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("mail")), ) .unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let mut stmt = conn .prepare("SELECT keywords FROM emails ORDER BY id") .unwrap(); @@ -2708,7 +2712,7 @@ fn drive_items_import_as_file_nodes_and_skip_facetless_items() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - let summary = vandelay::sync::import_exchange_graph::run( + let summary = inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("files")), ) @@ -2725,7 +2729,7 @@ fn drive_items_import_as_file_nodes_and_skip_facetless_items() { "one directory and two files; the remoteItem has no file or folder facet and is skipped" ); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let vault: i64 = conn .query_row( "SELECT count(*) FROM file_nodes WHERE name = 'Personal Vault'", @@ -2844,13 +2848,13 @@ fn a_deleted_occurrence_becomes_an_excluded_override() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - vandelay::sync::import_exchange_graph::run( + inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("calendar")), ) .unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let row: String = conn .query_row("SELECT data FROM calendar_events", [], |row| row.get(0)) .unwrap(); @@ -2910,13 +2914,13 @@ fn event_file_attachments_become_enclosure_links() { let base = server.url(); let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); - vandelay::sync::import_exchange_graph::run( + inbuxa_migrate::sync::import_exchange_graph::run( make_common(archive.clone()), make_config(base, None, surfaces("calendar")), ) .unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let row: String = conn .query_row("SELECT data FROM calendar_events", [], |row| row.get(0)) .unwrap(); @@ -2985,9 +2989,9 @@ fn contact_photo_categories_and_im_addresses_are_imported() { let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned(); let mut config = make_config(base, None, surfaces("contacts")); config.contact_photos = true; - vandelay::sync::import_exchange_graph::run(make_common(archive.clone()), config).unwrap(); + inbuxa_migrate::sync::import_exchange_graph::run(make_common(archive.clone()), config).unwrap(); - let conn = vandelay::db::init::open(&archive).unwrap(); + let conn = inbuxa_migrate::db::init::open(&archive).unwrap(); let row: String = conn .query_row("SELECT data FROM contact_cards", [], |row| row.get(0)) .unwrap(); diff --git a/tests/mock_imap.rs b/tests/mock_imap.rs index 4ec2ab7..b3ab835 100644 --- a/tests/mock_imap.rs +++ b/tests/mock_imap.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -13,14 +14,14 @@ use std::thread; use std::time::Duration; use encodify::base64::STANDARD; +use inbuxa_migrate::db; +use inbuxa_migrate::imap::client::{ConnectMode, ImapClient}; +use inbuxa_migrate::imap::error::ImapError; +use inbuxa_migrate::imap::transport::Connector; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::CommonConfig; +use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig, run}; use rusqlite::Connection; -use vandelay::db; -use vandelay::imap::client::{ConnectMode, ImapClient}; -use vandelay::imap::error::ImapError; -use vandelay::imap::transport::Connector; -use vandelay::logging::Logger; -use vandelay::sync::CommonConfig; -use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig, run}; type Script = Box std::io::Result<()> + Send + 'static>; @@ -129,7 +130,7 @@ fn connect_mock(server: &MockImap) -> Result { fn tempfile(label: &str) -> PathBuf { let counter = COUNTER.fetch_add(1, Ordering::SeqCst); let mut p = std::env::temp_dir(); - p.push(format!("vandelay_mock_imap_{label}_{counter}.sqlite")); + p.push(format!("inbuxa_migrate_mock_imap_{label}_{counter}.sqlite")); if p.exists() { let _ = std::fs::remove_file(&p); } @@ -143,7 +144,7 @@ fn run_import( user: &str, archive: PathBuf, config_tweak: impl FnOnce(&mut ImapImportConfig), -) -> Result { +) -> Result { let common = CommonConfig { archive: archive.clone(), threads: 1, @@ -364,14 +365,14 @@ fn run_collect_parses_fetch_with_literal_body() { .run_collect("UID FETCH 5 (UID BODY.PEEK[])") .expect("fetch"); let body = r.untagged.iter().find_map(|u| match u { - vandelay::imap::response::Untagged::Fetch { items, .. } => Some(items.clone()), + inbuxa_migrate::imap::response::Untagged::Fetch { items, .. } => Some(items.clone()), _ => None, }); let items = body.expect("fetch items"); let body_item = items.iter().find(|(n, _)| n == "BODY[]").unwrap(); match &body_item.1 { - vandelay::imap::response::Value::Str(s) => assert_eq!(s, "Hello world"), - vandelay::imap::response::Value::Bytes(b) => assert_eq!(b, b"Hello world"), + inbuxa_migrate::imap::response::Value::Str(s) => assert_eq!(s, "Hello world"), + inbuxa_migrate::imap::response::Value::Bytes(b) => assert_eq!(b, b"Hello world"), other => panic!("unexpected body shape: {other:?}"), } } @@ -1094,7 +1095,7 @@ fn coordinator_source_change_detected_on_different_url() { let server2 = MockImap::start_scripts(vec![control2]); let err = run_import(&server2, "alice", archive.clone(), |_| {}).unwrap_err(); - assert!(matches!(err, vandelay::error::Error::SourceChange(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_))); } #[test] @@ -1483,7 +1484,7 @@ fn coordinator_auth_plain_and_login_both_refused_aborts_run() { }); let archive = tempfile("dual_refuse"); let err = run_import(&server, "alice", archive, |_| {}).unwrap_err(); - assert!(matches!(err, vandelay::error::Error::Connection(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Connection(_))); assert_eq!(err.exit_code(), 2); } @@ -1812,7 +1813,7 @@ fn coordinator_authenticationfailed_yields_exit2_connection_error() { }); let archive = tempfile("authfail"); let err = run_import(&server, "alice", archive, |_| {}).unwrap_err(); - assert!(matches!(err, vandelay::error::Error::Connection(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Connection(_))); assert_eq!(err.exit_code(), 2); } diff --git a/tests/mock_jmap.rs b/tests/mock_jmap.rs index abc4fd7..b0ab867 100644 --- a/tests/mock_jmap.rs +++ b/tests/mock_jmap.rs @@ -1,22 +1,23 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use std::time::Instant; -use serde_json::json; -use vandelay::jmap::account::{self, AccountSelector}; -use vandelay::jmap::error::JmapError; -use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy}; -use vandelay::jmap::request::{ +use inbuxa_migrate::jmap::account::{self, AccountSelector}; +use inbuxa_migrate::jmap::error::JmapError; +use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy}; +use inbuxa_migrate::jmap::request::{ self, SetRequest, get_all, get_changes, get_objects, get_state, set_call, }; -use vandelay::jmap::session::{Limits, Session}; -use vandelay::jmap::wire::JmapId; -use vandelay::jmap::wire::identity::Identity; -use vandelay::jmap::wire::mailbox::Mailbox; +use inbuxa_migrate::jmap::session::{Limits, Session}; +use inbuxa_migrate::jmap::wire::JmapId; +use inbuxa_migrate::jmap::wire::identity::Identity; +use inbuxa_migrate::jmap::wire::mailbox::Mailbox; +use serde_json::json; fn client(retries: u32) -> HttpClient { HttpClient::new( @@ -672,7 +673,7 @@ fn retries_counter_increments_on_429() { #[test] fn upload_size_precheck_blocks_before_contacting_server() { - use vandelay::jmap::blobxfer; + use inbuxa_migrate::jmap::blobxfer; let mut server = mockito::Server::new(); let no_hits = server @@ -739,7 +740,7 @@ fn concurrent_requests_400_problem_is_retried_then_succeeds() { #[test] fn concurrent_uploads_400_problem_is_retried_then_succeeds() { - use vandelay::jmap::blobxfer; + use inbuxa_migrate::jmap::blobxfer; let mut server = mockito::Server::new(); server @@ -772,7 +773,7 @@ fn concurrent_uploads_400_problem_is_retried_then_succeeds() { #[test] fn blob_upload_quota_429_with_retry_after_is_honoured() { - use vandelay::jmap::blobxfer; + use inbuxa_migrate::jmap::blobxfer; let mut server = mockito::Server::new(); server @@ -883,7 +884,7 @@ fn ratelimit_headers_without_retry_after_fall_back_to_backoff() { #[test] fn upload_extracts_blob_id_and_download_streams_via_download_url() { - use vandelay::jmap::blobxfer; + use inbuxa_migrate::jmap::blobxfer; let mut server = mockito::Server::new(); let base = server.url(); @@ -974,7 +975,7 @@ fn persistent_problem_json_429_without_retry_after_eventually_succeeds_via_share #[test] fn persistent_429_on_download_uses_shared_throttle() { - use vandelay::jmap::blobxfer; + use inbuxa_migrate::jmap::blobxfer; let mut server = mockito::Server::new(); let session = upload_session(&server.url()); @@ -1207,7 +1208,7 @@ fn archive_path() -> std::path::PathBuf { let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-mockjmap-{}-{n}.sqlite", + "inbuxa-migrate-mockjmap-{}-{n}.sqlite", std::process::id() )); let _ = std::fs::remove_file(&p); @@ -1264,17 +1265,17 @@ fn import_aborts_with_exit_two_when_the_advertised_api_url_is_unreachable() { .create(); let archive = archive_path(); - let err = vandelay::sync::import_jmap::run( - vandelay::sync::CommonConfig { + let err = inbuxa_migrate::sync::import_jmap::run( + inbuxa_migrate::sync::CommonConfig { archive: archive.clone(), threads: 1, dry_run: false, max_retries: 0, allow_invalid_certs: false, - logger: vandelay::logging::Logger::from_flags(true, 0), + logger: inbuxa_migrate::logging::Logger::from_flags(true, 0), }, - vandelay::sync::ImportConfig { - connect: vandelay::sync::ConnectConfig { + inbuxa_migrate::sync::ImportConfig { + connect: inbuxa_migrate::sync::ConnectConfig { url: session_base.clone(), auth: Auth::Basic { user: "u".into(), @@ -1289,7 +1290,7 @@ fn import_aborts_with_exit_two_when_the_advertised_api_url_is_unreachable() { .expect_err("an unreachable apiUrl must abort the run"); assert!( - matches!(err, vandelay::error::Error::Connection(_)), + matches!(err, inbuxa_migrate::error::Error::Connection(_)), "a 404 from the advertised apiUrl is a whole-run connection failure, got {err:?}" ); assert_eq!(err.exit_code(), 2, "must not report a partial failure"); diff --git a/tests/mock_maildir.rs b/tests/mock_maildir.rs index 5697b3d..e37148f 100644 --- a/tests/mock_maildir.rs +++ b/tests/mock_maildir.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -8,16 +9,16 @@ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::CommonConfig; +use inbuxa_migrate::sync::import_maildir::{MaildirImportConfig, run}; use rusqlite::Connection; use tempfile::TempDir; -use vandelay::logging::Logger; -use vandelay::sync::CommonConfig; -use vandelay::sync::import_maildir::{MaildirImportConfig, run}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-mock-maildir-{tag}-{}-{}.sqlite", + "inbuxa-migrate-mock-maildir-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -530,7 +531,7 @@ fn rejects_path_without_cur_subdir() { fs::create_dir_all(td.path().join("new")).unwrap(); let archive = tmp_archive("not-a-maildir"); let err = run(common(&archive), base_cfg(td.path())).expect_err("should refuse"); - assert!(matches!(err, vandelay::error::Error::Usage(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_))); let _ = fs::remove_file(&archive); } @@ -544,7 +545,7 @@ fn rejects_dovecot_layout_fs_tree() { let archive = tmp_archive("layout-fs"); let err = run(common(&archive), base_cfg(td.path())).expect_err("should refuse"); match err { - vandelay::error::Error::Usage(msg) => { + inbuxa_migrate::error::Error::Usage(msg) => { assert!(msg.contains("Maildir++"), "msg was: {msg}"); } other => panic!("expected Usage error, got {other:?}"), @@ -558,7 +559,7 @@ fn rejects_nonexistent_path() { let mut cfg = base_cfg(Path::new("/definitely/not/a/real/maildir")); cfg.maildir = PathBuf::from("/definitely/not/a/real/maildir"); let err = run(common(&archive), cfg).expect_err("should refuse"); - assert!(matches!(err, vandelay::error::Error::Usage(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_))); let _ = fs::remove_file(&archive); } @@ -572,7 +573,7 @@ fn source_change_protection_refuses_second_path() { let archive = tmp_archive("source-change"); run(common(&archive), base_cfg(td_a.path())).expect("first import"); let err = run(common(&archive), base_cfg(td_b.path())).expect_err("second import refused"); - assert!(matches!(err, vandelay::error::Error::SourceChange(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_))); let _ = fs::remove_file(&archive); } @@ -601,7 +602,7 @@ fn folder_and_include_are_mutually_exclusive() { cfg.include = vec![regex::Regex::new("^Sent$").unwrap()]; let err = run(common(&archive), cfg).expect_err("mutex"); match err { - vandelay::error::Error::Usage(msg) => { + inbuxa_migrate::error::Error::Usage(msg) => { assert!( msg.contains("--folder") && msg.contains("--include"), "msg was: {msg}" @@ -621,7 +622,7 @@ fn folder_and_exclude_are_mutually_exclusive() { cfg.folder = vec!["INBOX".to_owned()]; cfg.exclude = vec![regex::Regex::new("^Trash$").unwrap()]; let err = run(common(&archive), cfg).expect_err("mutex"); - assert!(matches!(err, vandelay::error::Error::Usage(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_))); let _ = fs::remove_file(&archive); } diff --git a/tests/mock_managesieve.rs b/tests/mock_managesieve.rs index aa34e71..3f3bbd5 100644 --- a/tests/mock_managesieve.rs +++ b/tests/mock_managesieve.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -12,10 +13,10 @@ use std::sync::atomic::{AtomicU32, Ordering}; use std::thread; use std::time::Duration; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::CommonConfig; +use inbuxa_migrate::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig, run}; use rusqlite::Connection; -use vandelay::logging::Logger; -use vandelay::sync::CommonConfig; -use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig, run}; type Script = Box std::io::Result<()> + Send + 'static>; @@ -117,7 +118,7 @@ fn tempfile(label: &str) -> PathBuf { let counter = COUNTER.fetch_add(1, Ordering::SeqCst); let mut p = std::env::temp_dir(); p.push(format!( - "vandelay_mock_managesieve_{label}_{counter}.sqlite" + "inbuxa_migrate_mock_managesieve_{label}_{counter}.sqlite" )); if p.exists() { let _ = std::fs::remove_file(&p); @@ -133,7 +134,7 @@ fn count(conn: &Connection, table: &str) -> i64 { fn run_basic( server: &MockSieveServer, archive: &Path, -) -> Result { +) -> Result { let common = CommonConfig { archive: archive.to_path_buf(), threads: 1, @@ -256,7 +257,7 @@ fn auth_no_translates_to_connection_error() { let archive = tempfile("auth_fail"); let err = run_basic(&server, &archive).unwrap_err(); match err { - vandelay::error::Error::Connection(msg) => assert!( + inbuxa_migrate::error::Error::Connection(msg) => assert!( msg.contains("auth failed") || msg.contains("LOGIN"), "got {msg}" ), @@ -274,7 +275,7 @@ fn server_lacking_plain_login_for_basic_is_refused() { }); let archive = tempfile("no_basic_mech"); let err = run_basic(&server, &archive).unwrap_err(); - assert!(matches!(err, vandelay::error::Error::Connection(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Connection(_))); let _ = std::fs::remove_file(&archive); } @@ -423,7 +424,7 @@ fn referral_aborts_run_with_connection_error() { let archive = tempfile("referral"); let err = run_basic(&server, &archive).unwrap_err(); match err { - vandelay::error::Error::Connection(msg) => assert!(msg.contains("referral"), "{msg}"), + inbuxa_migrate::error::Error::Connection(msg) => assert!(msg.contains("referral"), "{msg}"), other => panic!("expected Connection, got {other:?}"), } let _ = std::fs::remove_file(&archive); @@ -438,7 +439,7 @@ fn seed_then_run(seed: Script, second: Script) -> (MockSieveServer, PathBuf) { fn run_against( server: &MockSieveServer, archive: &Path, -) -> Result { +) -> Result { run_basic(server, archive) } @@ -935,7 +936,7 @@ fn plain_then_login_both_refused_aborts_run_with_combined_message() { let archive = tempfile("plain_and_login_refused"); let err = run_basic(&server, &archive).unwrap_err(); match err { - vandelay::error::Error::Connection(msg) => { + inbuxa_migrate::error::Error::Connection(msg) => { assert!( msg.contains("LOGIN") && msg.contains("PLAIN"), "expected combined LOGIN/PLAIN error text, got {msg}" diff --git a/tests/mock_sync.rs b/tests/mock_sync.rs index 6396a74..2bd9546 100644 --- a/tests/mock_sync.rs +++ b/tests/mock_sync.rs @@ -1,26 +1,27 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use std::path::{Path, PathBuf}; +use inbuxa_migrate::db; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; +use inbuxa_migrate::types::ObjectType; use mockito::Matcher; use serde_json::{Value, json}; -use vandelay::db; -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; -use vandelay::types::ObjectType; fn tmp() -> PathBuf { static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-mocksync-{}-{:?}-{n}.sqlite", + "inbuxa-migrate-mocksync-{}-{:?}-{n}.sqlite", std::process::id(), std::thread::current().id(), )); @@ -1585,8 +1586,8 @@ fn export_missing_target_email_is_created_on_rerun() { let raw = format!("From: a@x\r\nSubject: m{n}\r\nMessage-ID: \r\n\r\nbody {n}",); let blob = db::blobs::intern_blob(&conn, raw.as_bytes()).unwrap(); - let mm = vandelay::sync::keys::index_to_json( - &vandelay::sync::emailmeta::email_index_from_blob(raw.as_bytes()), + let mm = inbuxa_migrate::sync::keys::index_to_json( + &inbuxa_migrate::sync::emailmeta::email_index_from_blob(raw.as_bytes()), ); conn.execute( "INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords,message_match) @@ -1700,8 +1701,8 @@ fn export_email_blake3_fallback_matches_when_no_message_id() { ) .unwrap(); let blob = db::blobs::intern_blob(&conn, raw.as_bytes()).unwrap(); - let mm = vandelay::sync::keys::index_to_json( - &vandelay::sync::emailmeta::email_index_from_blob(raw.as_bytes()), + let mm = inbuxa_migrate::sync::keys::index_to_json( + &inbuxa_migrate::sync::emailmeta::email_index_from_blob(raw.as_bytes()), ); conn.execute( "INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords,message_match) @@ -1711,7 +1712,7 @@ fn export_email_blake3_fallback_matches_when_no_message_id() { .unwrap(); } - let local_idx = vandelay::sync::emailmeta::email_index_from_blob(raw.as_bytes()); + let local_idx = inbuxa_migrate::sync::emailmeta::email_index_from_blob(raw.as_bytes()); assert!(local_idx.mids.is_empty(), "blob must lack Message-ID"); let _root = server.mock("GET", "/").with_status(404).create(); @@ -4144,7 +4145,7 @@ fn export_aborts_with_exit_two_when_the_advertised_api_url_is_unreachable() { .expect_err("an unreachable apiUrl must abort the run"); assert!( - matches!(err, vandelay::error::Error::Connection(_)), + matches!(err, inbuxa_migrate::error::Error::Connection(_)), "a 404 from the advertised apiUrl is a whole-run connection failure, got {err:?}" ); assert_eq!(err.exit_code(), 2, "must not report a partial failure"); @@ -4207,7 +4208,7 @@ fn aborting_import_still_reports_the_types_that_finished() { .as_ref() .expect("a 404 from the api endpoint aborts the run"); assert!( - matches!(err, vandelay::error::Error::Connection(_)), + matches!(err, inbuxa_migrate::error::Error::Connection(_)), "the abort must keep its own exit code, got {err:?}" ); assert_eq!(err.exit_code(), 2); diff --git a/tests/seed_smoke.rs b/tests/seed_smoke.rs index 5475b25..a535fbf 100644 --- a/tests/seed_smoke.rs +++ b/tests/seed_smoke.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -98,7 +99,9 @@ fn verify_account(label: &str, user: &Jmap, account_id: &str, s: &SeedStats) { assert_eq!(list.len(), 2, "{label}: sieve script count"); let primary = list .iter() - .find(|s| s.get("name").and_then(Value::as_str) == Some("vandelay-test-filter")) + .find(|s| { + s.get("name").and_then(Value::as_str) == Some("inbuxa-migrate-test-filter") + }) .expect("primary sieve script present"); assert_eq!( primary.get("isActive").and_then(Value::as_bool), diff --git a/tests/seeder/mod.rs b/tests/seeder/mod.rs index 8b557ab..f3dfec8 100644 --- a/tests/seeder/mod.rs +++ b/tests/seeder/mod.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -17,14 +18,14 @@ use seed::{FileSpec, Layout, MailboxSpec}; pub const ADMIN_USER: &str = "admin"; pub const ADMIN_PASSWORD: &str = "admin"; -pub const DOMAIN: &str = "vandelay.org"; -pub const USER_PASSWORD: &str = "VandelayUser#2026"; -pub const ADMIN_ACCOUNT_PASSWORD: &str = "VandelayAdmin#2026"; +pub const DOMAIN: &str = "inbuxa-migrate.org"; +pub const USER_PASSWORD: &str = "InbuxaUser#2026"; +pub const ADMIN_ACCOUNT_PASSWORD: &str = "InbuxaAdmin#2026"; pub const SYNC_IN: [&str; 3] = ["test1", "test2", "test3"]; pub const SYNC_OUT: [&str; 3] = ["test4", "test5", "test6"]; pub const ADMIN_LOCALPART: &str = "vandeladmin"; -pub const CUSTOM_IDENTITY_NAME: &str = "Vandelay Test (Custom)"; +pub const CUSTOM_IDENTITY_NAME: &str = "Inbuxa Test (Custom)"; macro_rules! mailbox { ($k:expr, $n:expr, $p:expr, $r:expr) => { diff --git a/tests/seeder/seed.rs b/tests/seeder/seed.rs index d93af57..091aef8 100644 --- a/tests/seeder/seed.rs +++ b/tests/seeder/seed.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -230,8 +231,8 @@ fn seed_sieve(jmap: &Jmap, account_id: &str, active: bool) -> SeedResult<()> { "SieveScript/set", account_id, json!({ - "s0": { "name": "vandelay-test-filter", "blobId": primary_blob }, - "s1": { "name": "vandelay-vacation", "blobId": secondary_blob } + "s0": { "name": "inbuxa-migrate-test-filter", "blobId": primary_blob }, + "s1": { "name": "inbuxa-migrate-vacation", "blobId": secondary_blob } }), &extra, )?; @@ -247,8 +248,8 @@ fn seed_identity(jmap: &Jmap, account_id: &str, account_email: &str) -> SeedResu "i0": { "name": super::CUSTOM_IDENTITY_NAME, "email": account_email, - "textSignature": "-- \nVandelay Industries", - "htmlSignature": "

Vandelay Industries

" + "textSignature": "-- \nInbuxa Industries", + "htmlSignature": "

Inbuxa Industries

" } }), &[], @@ -339,7 +340,7 @@ fn seed_contacts_calendars( &[CORE, CONTACTS], "AddressBook/set", account_id, - json!({ "ab": { "name": "Vandelay Work Contacts" } }), + json!({ "ab": { "name": "Inbuxa Work Contacts" } }), &[], )?; if let Some(id) = response @@ -359,7 +360,7 @@ fn seed_contacts_calendars( &[CORE, CALENDARS], "Calendar/set", account_id, - json!({ "cal": { "name": "Vandelay Team Calendar", "color": "#3366cc" } }), + json!({ "cal": { "name": "Inbuxa Team Calendar", "color": "#3366cc" } }), &[], )?; if let Some(id) = response diff --git a/tests/sync_caldav.rs b/tests/sync_caldav.rs index 2559816..76ded0e 100644 --- a/tests/sync_caldav.rs +++ b/tests/sync_caldav.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -16,16 +17,16 @@ use rusqlite::Connection; fn base_url() -> &'static str { shared_stalwart().base_url() } -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-{tag}-{}-{}.sqlite", + "inbuxa-migrate-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/tests/sync_carddav.rs b/tests/sync_carddav.rs index 49fd10e..6de2909 100644 --- a/tests/sync_carddav.rs +++ b/tests/sync_carddav.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -10,22 +11,22 @@ mod seeder; use std::collections::HashSet; use std::path::{Path, PathBuf}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; use integration::stalwart::shared as shared_stalwart; use rusqlite::Connection; -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; fn base_url() -> &'static str { shared_stalwart().base_url() } -use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; +use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-{tag}-{}-{}.sqlite", + "inbuxa-migrate-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/tests/sync_exchange_graph.rs b/tests/sync_exchange_graph.rs index 1d16a7e..749de26 100644 --- a/tests/sync_exchange_graph.rs +++ b/tests/sync_exchange_graph.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -9,15 +10,15 @@ mod seeder; use std::path::PathBuf; +use inbuxa_migrate::exchange_graph::types::{EventBodyFormat, MailboxKind, Surfaces}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_exchange_graph::{GraphAuth, GraphImportConfig}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig}; use integration::stalwart::shared as shared_stalwart; use mockito::{Matcher, Server}; use serde_json::{Value, json}; -use vandelay::exchange_graph::types::{EventBodyFormat, MailboxKind, Surfaces}; -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::import_exchange_graph::{GraphAuth, GraphImportConfig}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig}; fn base_url() -> &'static str { shared_stalwart().base_url() @@ -26,7 +27,7 @@ fn base_url() -> &'static str { fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-{tag}-{}-{}.sqlite", + "inbuxa-migrate-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -96,9 +97,9 @@ fn graph_fixture(server: &mut Server) { .with_status(200) .with_header("content-type", "text/plain") .with_body( - "From: art@vandelay.example\r\nTo: alice@x.com\r\n\ + "From: art@inbuxa-migrate.example\r\nTo: alice@x.com\r\n\ Subject: Graph round trip\r\nDate: Tue, 01 Sep 2026 10:00:00 +0000\r\n\ - Message-ID: \r\n\ + Message-ID: \r\n\ MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n\ Grüße aus Köln 🎉\r\n", ) @@ -219,8 +220,8 @@ fn graph_fixture(server: &mut Server) { server, "/me/contacts/C1", r#"{"id":"C1","displayName":"Graph Contact","givenName":"Graph","surname":"Contact", - "categories":["Work","VIP"],"imAddresses":["sip:graph@vandelay.example"], - "emailAddresses":[{"address":"graph@vandelay.example"}]}"#, + "categories":["Work","VIP"],"imAddresses":["sip:graph@inbuxa-migrate.example"], + "emailAddresses":[{"address":"graph@inbuxa-migrate.example"}]}"#, ); server .mock("GET", "/me/contacts/C1/photo/$value") @@ -393,7 +394,7 @@ fn graph_import_survives_a_full_export_to_stalwart() { .as_object() .and_then(|m| m.values().next()) .and_then(|s| s["uri"].as_str()), - Some("sip:graph@vandelay.example"), + Some("sip:graph@inbuxa-migrate.example"), "a URI-shaped IM address must land in uri: Stalwart drops an OnlineService \ that carries only user, even though RFC 9553 permits it" ); diff --git a/tests/sync_imap.rs b/tests/sync_imap.rs index 5eaf6f8..c1b93fd 100644 --- a/tests/sync_imap.rs +++ b/tests/sync_imap.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -22,16 +23,16 @@ fn imaps_url() -> String { format!("imaps://{}:{}", s.host, s.imaps_port) } -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-imap-{tag}-{}-{}.sqlite", + "inbuxa-migrate-imap-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/tests/sync_jmap.rs b/tests/sync_jmap.rs index bd51ef4..842dab1 100644 --- a/tests/sync_jmap.rs +++ b/tests/sync_jmap.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -10,15 +11,15 @@ mod seeder; use std::path::{Path, PathBuf}; use encodify::base64::STANDARD; +use inbuxa_migrate::jmap::account::{self, AccountSelector}; +use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy}; +use inbuxa_migrate::jmap::request::Request; +use inbuxa_migrate::jmap::session::Session; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; use integration::stalwart::shared as shared_stalwart; use rusqlite::Connection; use serde_json::{Map, Value, json}; -use vandelay::jmap::account::{self, AccountSelector}; -use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy}; -use vandelay::jmap::request::Request; -use vandelay::jmap::session::Session; -use vandelay::logging::Logger; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; fn base_url() -> &'static str { shared_stalwart().base_url() @@ -27,7 +28,7 @@ fn base_url() -> &'static str { fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-{tag}-{}-{}.sqlite", + "inbuxa-migrate-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -181,7 +182,7 @@ fn import_resolves_account_via_admin_principal_path() { }, account: AccountSelector::Name(target.email.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }; let summary = sync::import_jmap::run(common(&archive, false), cfg) @@ -390,7 +391,7 @@ fn import_export_deeply_nested_mailbox_tree() { auth: basic("test4"), account: AccountSelector::Id(src.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }; let s = sync::import_jmap::run(common(&archive, false), imp).expect("import"); @@ -432,7 +433,7 @@ fn import_export_deeply_nested_mailbox_tree() { auth: basic("test5"), account: AccountSelector::Id(tgt.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }; sync::import_jmap::run(common(&again_archive, false), imp_again) @@ -470,7 +471,7 @@ fn import_export_deeply_nested_filenode_tree() { auth: basic("test4"), account: AccountSelector::Id(src.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::FileNode]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::FileNode]), allow_source_change: false, }; let s = sync::import_jmap::run(common(&archive, false), imp).expect("import"); @@ -514,7 +515,7 @@ fn import_export_deeply_nested_filenode_tree() { auth: basic("test5"), account: AccountSelector::Id(tgt.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::FileNode]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::FileNode]), allow_source_change: false, }, ) @@ -554,7 +555,7 @@ fn prune_multilevel_mailbox_tree_destroys_leaf_first() { auth: basic("test4"), account: AccountSelector::Id(src.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }, ) @@ -567,7 +568,7 @@ fn prune_multilevel_mailbox_tree_destroys_leaf_first() { let arch_empty = tmp_archive("prune_deep_empty"); { - let _ = vandelay::db::init::open(&arch_empty).expect("init empty archive"); + let _ = inbuxa_migrate::db::init::open(&arch_empty).expect("init empty archive"); } let small_jmap = jmap_for(&fx.account("test6").expect("test6").localpart); @@ -595,7 +596,7 @@ fn prune_multilevel_mailbox_tree_destroys_leaf_first() { auth: basic("test6"), account: AccountSelector::Id(fx.account("test6").unwrap().account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }, ) @@ -644,8 +645,8 @@ fn prune_non_tree_type_contact_card() { account: AccountSelector::Id(src.account_id.clone()), }, objects: Some(vec![ - vandelay::types::ObjectType::AddressBook, - vandelay::types::ObjectType::ContactCard, + inbuxa_migrate::types::ObjectType::AddressBook, + inbuxa_migrate::types::ObjectType::ContactCard, ]), allow_source_change: false, }, @@ -659,7 +660,7 @@ fn prune_non_tree_type_contact_card() { let arch_empty = tmp_archive("prune_cc_empty"); { - let conn = vandelay::db::init::open(&arch_empty).expect("init empty archive"); + let conn = inbuxa_migrate::db::init::open(&arch_empty).expect("init empty archive"); conn.execute( "INSERT INTO address_books (id,name,is_default) VALUES (1,'AB',1)", [], @@ -690,8 +691,8 @@ fn prune_non_tree_type_contact_card() { seeder::teardown(base_url()).expect("teardown"); } -const ISSUE30_CARD_UID: &str = "vandelay-issue30-card"; -const ISSUE30_EVENT_UID: &str = "vandelay-issue30-event"; +const ISSUE30_CARD_UID: &str = "inbuxa-migrate-issue30-card"; +const ISSUE30_EVENT_UID: &str = "inbuxa-migrate-issue30-event"; fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> Vec { let uri = resource @@ -716,10 +717,10 @@ fn export_inlines_contact_and_event_blobs_instead_of_blob_ids() { let tgt = fx.account("test6").expect("test6"); let archive = tmp_archive("issue30"); - let photo: &[u8] = b"\x89PNG\r\n\x1a\nvandelay issue 30 contact photo bytes"; - let agenda: &[u8] = b"vandelay issue 30 calendar enclosure bytes"; + let photo: &[u8] = b"\x89PNG\r\n\x1a\ninbuxa-migrate issue 30 contact photo bytes"; + let agenda: &[u8] = b"inbuxa-migrate issue 30 calendar enclosure bytes"; { - let conn = vandelay::db::init::open(&archive).expect("init archive"); + let conn = inbuxa_migrate::db::init::open(&archive).expect("init archive"); conn.execute( "INSERT INTO address_books (id,name,is_default) VALUES (1,'Issue30 Book',1)", [], @@ -730,8 +731,8 @@ fn export_inlines_contact_and_event_blobs_instead_of_blob_ids() { [], ) .unwrap(); - let photo_blob = vandelay::db::blobs::intern_blob(&conn, photo).unwrap(); - let agenda_blob = vandelay::db::blobs::intern_blob(&conn, agenda).unwrap(); + let photo_blob = inbuxa_migrate::db::blobs::intern_blob(&conn, photo).unwrap(); + let agenda_blob = inbuxa_migrate::db::blobs::intern_blob(&conn, agenda).unwrap(); let card = json!({ "@type": "Card", "version": "1.0", @@ -944,7 +945,7 @@ fn export_prune_destroys_unmatched() { auth: basic("test1"), account: AccountSelector::Id(big.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }, ) @@ -964,7 +965,7 @@ fn export_prune_destroys_unmatched() { auth: basic("test3"), account: AccountSelector::Id(small.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::Mailbox]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]), allow_source_change: false, }, ) @@ -1004,7 +1005,7 @@ fn import_resume_converges_after_partial() { }; let mut first = mk(); - first.objects = Some(vec![vandelay::types::ObjectType::Mailbox]); + first.objects = Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]); sync::import_jmap::run(common(&archive, false), first).expect("partial import"); let conn = Connection::open(&archive).unwrap(); @@ -1038,10 +1039,10 @@ fn import_resume_converges_after_partial() { #[test] #[ignore = "requires Docker"] fn live_burst_exceeds_concurrent_requests_and_recovers() { + use inbuxa_migrate::jmap::http::{HttpClient, RetryPolicy}; + use inbuxa_migrate::jmap::request::{Request, check_method_error}; + use inbuxa_migrate::jmap::session::{Limits, Session}; use std::sync::Arc; - use vandelay::jmap::http::{HttpClient, RetryPolicy}; - use vandelay::jmap::request::{Request, check_method_error}; - use vandelay::jmap::session::{Limits, Session}; let fx = seeder::provision(base_url()).expect("provision"); let acc = fx.account("test1").expect("test1"); @@ -1131,7 +1132,7 @@ fn live_burst_exceeds_concurrent_requests_and_recovers() { } struct JmapSettingsGuard { - admin: vandelay::jmap::http::HttpClient, + admin: inbuxa_migrate::jmap::http::HttpClient, admin_api: String, admin_account: String, previous: serde_json::Map, @@ -1139,8 +1140,8 @@ struct JmapSettingsGuard { impl JmapSettingsGuard { fn override_settings(updates: serde_json::Map) -> Self { - use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy}; - use vandelay::jmap::session::Session; + use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy}; + use inbuxa_migrate::jmap::session::Session; let admin = HttpClient::new( Auth::Basic { user: seeder::ADMIN_USER.to_owned(), @@ -1188,7 +1189,7 @@ impl Drop for JmapSettingsGuard { } fn read_jmap_settings( - admin: &vandelay::jmap::http::HttpClient, + admin: &inbuxa_migrate::jmap::http::HttpClient, api: &str, account: &str, properties: &[&str], @@ -1217,7 +1218,7 @@ fn read_jmap_settings( } fn apply_jmap_settings( - admin: &vandelay::jmap::http::HttpClient, + admin: &inbuxa_migrate::jmap::http::HttpClient, api: &str, account: &str, updates: &serde_json::Map, @@ -1252,9 +1253,9 @@ fn apply_jmap_settings( #[test] #[ignore = "requires Docker"] fn live_blob_quota_429_triggers_retry_after_then_succeeds() { - use vandelay::jmap::blobxfer; - use vandelay::jmap::http::{HttpClient, RetryPolicy}; - use vandelay::jmap::session::Session; + use inbuxa_migrate::jmap::blobxfer; + use inbuxa_migrate::jmap::http::{HttpClient, RetryPolicy}; + use inbuxa_migrate::jmap::session::Session; let fx = seeder::provision(base_url()).expect("provision"); let acc = fx.account("test1").expect("test1"); diff --git a/tests/sync_maildir.rs b/tests/sync_maildir.rs index ad570e0..1ba182e 100644 --- a/tests/sync_maildir.rs +++ b/tests/sync_maildir.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -19,16 +20,16 @@ use rusqlite::Connection; fn base_url() -> &'static str { shared_stalwart().base_url() } -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::import_maildir::{MaildirImportConfig, run as run_maildir}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ImportConfig}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_maildir::{MaildirImportConfig, run as run_maildir}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ImportConfig}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-sync-maildir-{tag}-{}-{}.sqlite", + "inbuxa-migrate-sync-maildir-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -551,8 +552,8 @@ fn maildir_and_jmap_imports_share_blob_bytes() { account: AccountSelector::Id(acc.account_id.clone()), }, objects: Some(vec![ - vandelay::types::ObjectType::Mailbox, - vandelay::types::ObjectType::Email, + inbuxa_migrate::types::ObjectType::Mailbox, + inbuxa_migrate::types::ObjectType::Email, ]), allow_source_change: false, }; @@ -627,8 +628,8 @@ fn maildir_message_count_matches_jmap_for_same_corpus() { account: AccountSelector::Id(acc.account_id.clone()), }, objects: Some(vec![ - vandelay::types::ObjectType::Mailbox, - vandelay::types::ObjectType::Email, + inbuxa_migrate::types::ObjectType::Mailbox, + inbuxa_migrate::types::ObjectType::Email, ]), allow_source_change: false, }; diff --git a/tests/sync_managesieve.rs b/tests/sync_managesieve.rs index 4050740..ade8a57 100644 --- a/tests/sync_managesieve.rs +++ b/tests/sync_managesieve.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -25,16 +26,16 @@ fn sieves_url() -> String { let s = shared_stalwart(); format!("sieves://{}:{}", s.host, s.sieve_port) } -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ImportConfig}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ImportConfig}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-managesieve-{tag}-{}-{}.sqlite", + "inbuxa-migrate-managesieve-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -226,7 +227,7 @@ fn managesieve_and_jmap_imports_share_blob_bytes() { }, account: AccountSelector::Id(acc.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::SieveScript]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::SieveScript]), allow_source_change: false, }; let jmap_common = CommonConfig { @@ -319,7 +320,7 @@ fn managesieve_source_change_protection_refuses_second_account() { let err = sync::import_managesieve::run(common(&archive), basic_config(&acc2.localpart)) .expect_err("should refuse the second source without override"); - assert!(matches!(err, vandelay::error::Error::SourceChange(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_))); let _ = std::fs::remove_file(&archive); seeder::teardown(base_url()).expect("teardown"); @@ -336,7 +337,7 @@ fn managesieve_implicit_tls_path_succeeds_when_offered() { cfg.url = sieves_url(); let result = sync::import_managesieve::run(common(&archive), cfg); - if let Err(vandelay::error::Error::Connection(msg)) = &result { + if let Err(inbuxa_migrate::error::Error::Connection(msg)) = &result { eprintln!("(expected on cleartext-only deployments) {msg}"); } @@ -382,7 +383,7 @@ fn managesieve_round_trip_via_jmap_export_converges() { allow_invalid_certs: true, logger: Logger::from_flags(false, 0), }; - let export_cfg = vandelay::sync::ExportConfig { + let export_cfg = inbuxa_migrate::sync::ExportConfig { connect: ConnectConfig { url: fx.base_url.clone(), auth: Auth::Basic { @@ -391,7 +392,7 @@ fn managesieve_round_trip_via_jmap_export_converges() { }, account: AccountSelector::Id(dst.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::SieveScript]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::SieveScript]), prune: false, yes: false, }; @@ -416,7 +417,7 @@ fn managesieve_round_trip_via_jmap_export_converges() { allow_invalid_certs: true, logger: Logger::from_flags(false, 0), }; - let export_cfg2 = vandelay::sync::ExportConfig { + let export_cfg2 = inbuxa_migrate::sync::ExportConfig { connect: ConnectConfig { url: fx.base_url.clone(), auth: Auth::Basic { @@ -425,7 +426,7 @@ fn managesieve_round_trip_via_jmap_export_converges() { }, account: AccountSelector::Id(dst.account_id.clone()), }, - objects: Some(vec![vandelay::types::ObjectType::SieveScript]), + objects: Some(vec![inbuxa_migrate::types::ObjectType::SieveScript]), prune: false, yes: false, }; diff --git a/tests/sync_takeout.rs b/tests/sync_takeout.rs index 4d4a6b0..2cf82ab 100644 --- a/tests/sync_takeout.rs +++ b/tests/sync_takeout.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -11,9 +12,9 @@ use std::path::{Path, PathBuf}; use rusqlite::Connection; use tempfile::TempDir; -use vandelay::logging::Logger; -use vandelay::sync::CommonConfig; -use vandelay::sync::import_takeout::{TakeoutImportConfig, run}; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::CommonConfig; +use inbuxa_migrate::sync::import_takeout::{TakeoutImportConfig, run}; const RESOURCES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/resources"); @@ -230,7 +231,7 @@ fn empty_directory_fails_with_usage() { let archive = td.path().join("a.sqlite"); let err = run(common_for(&archive), config_for(td.path().to_path_buf())).unwrap_err(); assert!(format!("{err}").contains("no .mbox")); - assert!(matches!(err, vandelay::error::Error::Usage(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_))); assert_eq!(err.exit_code(), 1); } @@ -240,7 +241,7 @@ fn nonexistent_path_fails_with_usage() { let archive = td.path().join("a.sqlite"); let missing = td.path().join("does-not-exist"); let err = run(common_for(&archive), config_for(missing)).unwrap_err(); - assert!(matches!(err, vandelay::error::Error::Usage(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_))); assert_eq!(err.exit_code(), 1); } @@ -450,7 +451,7 @@ fn contacts_dedup_by_uid_across_two_vcf_files() { assert_eq!(with_uid, 2); let synthetic_uids: i64 = conn .query_row( - "SELECT COUNT(*) FROM contact_cards WHERE uid LIKE 'vandelay-syn-%'", + "SELECT COUNT(*) FROM contact_cards WHERE uid LIKE 'inbuxa-migrate-syn-%'", [], |r| r.get(0), ) @@ -543,7 +544,7 @@ fn source_change_protection_refuses_different_path() { let archive = td.path().join("a.sqlite"); run(common_for(&archive), config_for(a.clone())).unwrap(); let err = run(common_for(&archive), config_for(b.clone())).unwrap_err(); - assert!(matches!(err, vandelay::error::Error::SourceChange(_))); + assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_))); let mut cfg2 = config_for(b); cfg2.allow_source_change = true; diff --git a/tests/sync_webdav.rs b/tests/sync_webdav.rs index e3e724e..a2d994e 100644 --- a/tests/sync_webdav.rs +++ b/tests/sync_webdav.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -16,16 +17,16 @@ use rusqlite::Connection; fn base_url() -> &'static str { shared_stalwart().base_url() } -use vandelay::jmap::account::AccountSelector; -use vandelay::jmap::http::Auth; -use vandelay::logging::Logger; -use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; -use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; +use inbuxa_migrate::jmap::account::AccountSelector; +use inbuxa_migrate::jmap::http::Auth; +use inbuxa_migrate::logging::Logger; +use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg}; +use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig}; fn tmp_archive(tag: &str) -> PathBuf { let mut p = std::env::temp_dir(); p.push(format!( - "vandelay-{tag}-{}-{}.sqlite", + "inbuxa-migrate-{tag}-{}-{}.sqlite", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) From 7fbcf5031e2ae8aa470c9ae383fb4c14ec083290 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 10:09:10 -0700 Subject: [PATCH 2/6] Take the registry capability from the server's session Registry calls (x:Account, x:Domain and the rest of the x: types) were always sent under urn:stalwart:jmap. inbuxa advertises the same registry as urn:inbuxa:jmap:registry, so the capability now comes from the connected server: Session::registry_urn() picks urn:inbuxa:jmap:registry when the session advertises it, else urn:stalwart:jmap, so Stalwart servers still work as a source. A request carrying an x: call against a server that advertises neither fails with a MissingCapability error naming both, treated like any other connection-level failure, instead of sending a capability the server never offered. --- src/jmap/error.rs | 8 +++- src/jmap/request.rs | 104 ++++++++++++++++++++++++++++++++++++-------- src/jmap/session.rs | 42 ++++++++++++++++++ 3 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/jmap/error.rs b/src/jmap/error.rs index 9147c2e..48ff7cf 100644 --- a/src/jmap/error.rs +++ b/src/jmap/error.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -50,6 +51,9 @@ pub enum JmapError { #[error("malformed jmap response: {0}")] Malformed(String), + #[error("server lacks a required capability: {0}")] + MissingCapability(String), + #[error("json error: {0}")] Json(#[from] serde_json::Error), @@ -69,7 +73,9 @@ impl JmapError { impl From for Error { fn from(value: JmapError) -> Self { match value { - JmapError::Connect(m) | JmapError::Transport(m) => Error::Connection(m), + JmapError::Connect(m) | JmapError::Transport(m) | JmapError::MissingCapability(m) => { + Error::Connection(m) + } JmapError::Auth(m) => Error::Connection(format!("authentication rejected: {m}")), JmapError::Sqlite(e) => Error::Db(OpenError::Sqlite(e)), reached @ (JmapError::HttpStatus { .. } | JmapError::RetriesExhausted(_)) => { diff --git a/src/jmap/request.rs b/src/jmap/request.rs index 9cec161..c576530 100644 --- a/src/jmap/request.rs +++ b/src/jmap/request.rs @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * SPDX-FileCopyrightText: 2026 John Coffey * * SPDX-License-Identifier: Apache-2.0 OR MIT */ @@ -17,9 +18,28 @@ use crate::logging::Logger; pub const URN_CORE: &str = "urn:ietf:params:jmap:core"; -pub fn using_urn(method: &str) -> &'static str { +/// The capability inbuxa advertises for its registry types (`x:Account`, +/// `x:Domain` and the rest of the `x:` objects). +pub const URN_INBUXA_REGISTRY: &str = "urn:inbuxa:jmap:registry"; + +/// The same registry as Stalwart advertises it, so a Stalwart server can be +/// migrated from. +pub const URN_STALWART_REGISTRY: &str = "urn:stalwart:jmap"; + +/// The capability a method needs. Registry (`x:`) methods have no fixed +/// capability: the connected server names it, and `registry` carries that +/// name (see `Session::registry_urn`). Without one they cannot be sent. +pub fn using_urn(method: &str, registry: Option<&'static str>) -> Result<&'static str, JmapError> { let prefix = method.split('/').next().unwrap_or(method); - match prefix { + if prefix.starts_with("x:") { + return registry.ok_or_else(|| { + JmapError::MissingCapability(format!( + "{method} needs the server's registry capability, but it advertises neither \ + {URN_INBUXA_REGISTRY} nor {URN_STALWART_REGISTRY}" + )) + }); + } + Ok(match prefix { "Mailbox" | "Email" => "urn:ietf:params:jmap:mail", "Identity" => "urn:ietf:params:jmap:submission", "SieveScript" => "urn:ietf:params:jmap:sieve", @@ -27,9 +47,8 @@ pub fn using_urn(method: &str) -> &'static str { "Calendar" | "CalendarEvent" | "ParticipantIdentity" => "urn:ietf:params:jmap:calendars", "FileNode" => "urn:ietf:params:jmap:filenode", "Principal" => "urn:ietf:params:jmap:principals", - "x:Account" | "x:Domain" => "urn:stalwart:jmap", _ => URN_CORE, - } + }) } #[derive(Debug, Clone)] @@ -42,11 +61,19 @@ pub struct MethodCall { #[derive(Debug, Clone, Default)] pub struct Request { calls: Vec, + registry: Option<&'static str>, } impl Request { pub fn new() -> Request { - Request { calls: Vec::new() } + Request::default() + } + + /// The registry capability to use for any `x:` call in this request, + /// normally `session.registry_urn()`. + pub fn registry(&mut self, urn: Option<&'static str>) -> &mut Request { + self.registry = urn; + self } pub fn call( @@ -71,29 +98,29 @@ impl Request { self.calls.is_empty() } - pub fn using(&self) -> Vec { + pub fn using(&self) -> Result, JmapError> { let mut set: IndexSet = IndexSet::new(); set.insert(URN_CORE.to_owned()); for c in &self.calls { - set.insert(using_urn(&c.name).to_owned()); + set.insert(using_urn(&c.name, self.registry)?.to_owned()); } - set.into_iter().collect() + Ok(set.into_iter().collect()) } - fn envelope(&self) -> Value { + fn envelope(&self) -> Result { let method_calls: Vec = self .calls .iter() .map(|c| json!([c.name, c.args, c.call_id])) .collect(); - json!({ "using": self.using(), "methodCalls": method_calls }) + Ok(json!({ "using": self.using()?, "methodCalls": method_calls })) } pub fn fits(&self, limits: &Limits) -> Result<(), JmapError> { if self.calls.len() as u64 > limits.max_calls_in_request { return Err(JmapError::RequestTooLarge); } - let size = serde_json::to_vec(&self.envelope())?.len() as u64; + let size = serde_json::to_vec(&self.envelope()?)?.len() as u64; if size > limits.max_size_request { return Err(JmapError::RequestTooLarge); } @@ -101,7 +128,7 @@ impl Request { } pub fn send(&self, client: &HttpClient, api_url: &str) -> Result { - let value = client.post_json(api_url, &self.envelope())?; + let value = client.post_json(api_url, &self.envelope()?)?; Response::parse(value) } } @@ -855,24 +882,65 @@ mod tests { let mut r = Request::new(); r.call("Mailbox/get", json!({}), "a"); r.call("Email/query", json!({}), "b"); - let u = r.using(); + let u = r.using().unwrap(); assert!(u.contains(&URN_CORE.to_owned())); assert!(u.contains(&"urn:ietf:params:jmap:mail".to_owned())); assert_eq!(u.iter().filter(|x| x.as_str() == URN_CORE).count(), 1); } #[test] - fn using_maps_principal_and_stalwart() { + fn using_maps_standard_types() { assert_eq!( - using_urn("Principal/query"), + using_urn("Principal/query", None).unwrap(), "urn:ietf:params:jmap:principals" ); - assert_eq!(using_urn("x:Account/set"), "urn:stalwart:jmap"); assert_eq!( - using_urn("CalendarEvent/set"), + using_urn("CalendarEvent/set", None).unwrap(), "urn:ietf:params:jmap:calendars" ); - assert_eq!(using_urn("Identity/get"), "urn:ietf:params:jmap:submission"); + assert_eq!( + using_urn("Identity/get", None).unwrap(), + "urn:ietf:params:jmap:submission" + ); + } + + #[test] + fn registry_calls_use_the_inbuxa_capability() { + let mut r = Request::new(); + r.registry(Some(URN_INBUXA_REGISTRY)); + r.call("x:Account/set", json!({}), "a"); + r.call("x:Domain/get", json!({}), "b"); + assert_eq!(r.using().unwrap(), vec![URN_CORE, URN_INBUXA_REGISTRY]); + } + + #[test] + fn registry_calls_use_the_stalwart_capability_from_a_stalwart_source() { + let mut r = Request::new(); + r.registry(Some(URN_STALWART_REGISTRY)); + r.call("x:Account/get", json!({}), "a"); + assert_eq!(r.using().unwrap(), vec![URN_CORE, URN_STALWART_REGISTRY]); + } + + #[test] + fn registry_calls_without_a_registry_capability_fail_clearly() { + let mut r = Request::new(); + r.call("Mailbox/get", json!({}), "a"); + r.call("x:Domain/set", json!({}), "b"); + let err = r.using().unwrap_err(); + let msg = err.to_string(); + assert!(matches!(err, JmapError::MissingCapability(_))); + assert!(msg.contains("x:Domain/set"), "{msg}"); + assert!( + msg.contains(URN_INBUXA_REGISTRY) && msg.contains(URN_STALWART_REGISTRY), + "{msg}" + ); + } + + #[test] + fn requests_without_registry_calls_need_no_registry() { + let mut r = Request::new(); + r.call("Email/get", json!({}), "a"); + assert!(r.using().is_ok()); } #[test] diff --git a/src/jmap/session.rs b/src/jmap/session.rs index 32b584a..f0098df 100644 --- a/src/jmap/session.rs +++ b/src/jmap/session.rs @@ -12,6 +12,7 @@ use serde_json::Value; use crate::error::Error; use crate::jmap::error::JmapError; use crate::jmap::http::HttpClient; +use crate::jmap::request::{URN_INBUXA_REGISTRY, URN_STALWART_REGISTRY}; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -234,6 +235,14 @@ impl Session { .map_err(|e| Error::Connection(format!("session core capability is malformed: {e}"))) } + /// The capability to use for registry (`x:`) calls on this server: + /// inbuxa's name when advertised, else Stalwart's, else none. + pub fn registry_urn(&self) -> Option<&'static str> { + [URN_INBUXA_REGISTRY, URN_STALWART_REGISTRY] + .into_iter() + .find(|urn| self.capabilities.contains_key(*urn)) + } + pub fn account(&self, account_id: &str) -> Option<&Account> { self.accounts.get(account_id) } @@ -316,6 +325,39 @@ mod tests { }"# } + fn session_with(extra_caps: &[&str]) -> Session { + let mut v: serde_json::Value = serde_json::from_str(raw_session()).unwrap(); + for urn in extra_caps { + v["capabilities"][*urn] = serde_json::json!({}); + } + serde_json::from_value(v).unwrap() + } + + #[test] + fn registry_urn_prefers_inbuxa() { + assert_eq!( + session_with(&[URN_INBUXA_REGISTRY]).registry_urn(), + Some(URN_INBUXA_REGISTRY) + ); + assert_eq!( + session_with(&[URN_STALWART_REGISTRY, URN_INBUXA_REGISTRY]).registry_urn(), + Some(URN_INBUXA_REGISTRY) + ); + } + + #[test] + fn registry_urn_falls_back_to_stalwart() { + assert_eq!( + session_with(&[URN_STALWART_REGISTRY]).registry_urn(), + Some(URN_STALWART_REGISTRY) + ); + } + + #[test] + fn registry_urn_is_none_without_either() { + assert_eq!(session_with(&[]).registry_urn(), None); + } + #[test] fn parses_a_session_and_reads_limits() { let session = parse_session(raw_session()).unwrap(); From 42413563ef427bc0e57ea89513a6b77b1a76d691 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 10:12:48 -0700 Subject: [PATCH 3/6] Replace the release tooling with the suite's CI Upstream's cargo-dist pipeline published to npm and Homebrew under its own names, built an MSI, and ran on every pull request. It goes, with its dist-workspace.toml, the wix/ installer files and the README image. CI now follows the rest of the suite. .gitea/workflows runs `cargo test --locked` on Gitea's runners, or, when the org variable BUILD_ON is 'github', waits for the result GitHub reports on the commit. On GitHub, .github/workflows/ci.yml runs the same tests, and for a v* tag on main whose version matches Cargo.toml builds scripts/build-release.sh on native amd64 and arm64 runners (Ubuntu 22.04, for the widest glibc range), attaches inbuxa-migrate-linux-{amd64,arm64}.tar.gz and SHA256SUMS to the Gitea Release, copies the Release to GitHub, and reports back. Releases are built only on GitHub; with BUILD_ON unset a tag is tested but not released. Published releases are announced on the forum. --- .github/workflows/ci.yml | 257 +++++++++++++++++++++++++ .github/workflows/release.yml | 342 ---------------------------------- assets/importer-exporter.jpg | Bin 7754 -> 0 bytes dist-workspace.toml | 16 -- scripts/build-release.sh | 45 +++++ wix/main.wxs | 228 ----------------------- 6 files changed, 302 insertions(+), 586 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 assets/importer-exporter.jpg delete mode 100644 dist-workspace.toml create mode 100755 scripts/build-release.sh delete mode 100644 wix/main.wxs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c4a57f3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,257 @@ +# SPDX-FileCopyrightText: 2026 John Coffey +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# CI on GitHub Actions, for the GitHub copy of this repository. +# +# The repository lives on the self-hosted Gitea; GitHub holds a push mirror +# that Gitea updates on every commit. Nothing is merged here -- pull requests +# happen on Gitea, and their branches reach GitHub as ordinary pushes, which is +# why this workflow runs on `push` and not on `pull_request`. +# +# Which forge does the building is one switch, the variable BUILD_ON, set on +# both forges at the organization level: +# +# BUILD_ON=github every job here runs; .gitea/workflows/ci.yml skips its +# test job and waits for the status this workflow reports +# back instead. +# unset / other every job here skips; Gitea runs the tests. Releases need +# GitHub: they are built on native amd64 and arm64 runners. +# +# The result goes back to Gitea as one commit status, "github/ci (branch)" or +# "github/ci (tag)", which is what Gitea's `github` job waits on. +# +# v* tags build a release: the tag must be on main and name the version in +# Cargo.toml; each architecture is built on its own native runner by +# scripts/build-release.sh, and the archives plus SHA256SUMS are attached to +# the Gitea Release -- Gitea is where the install guide points. The Release is +# created as a draft, filled, then published, so it is never visible with +# assets missing; if an upload fails, a Release this run created is deleted. +# Gitea announces the release once this run has reported success. +# +# The binaries link glibc dynamically, so they are built on Ubuntu 22.04: the +# oldest glibc GitHub offers, and so the widest range of servers they run on. +# +# Configuration, all at the organization level on GitHub: variables BUILD_ON, +# GITEA_URL; secret GITEA_TOKEN (write:repository on Gitea). +# +# Every `uses:` is pinned to a full commit SHA, with the release it was in the +# trailing comment. Nothing schedules here: schedules belong to Gitea. +name: ci + +on: + push: + branches: ['**'] + tags: ['**'] + # For a run GitHub queued and then orphaned. Run it from a tag to redo that + # tag's release; attaching skips any file already on the Release. + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + +permissions: + contents: read + +env: + GITEA_URL: ${{ vars.GITEA_URL }} + CONTEXT: github/ci (${{ github.ref_type }}) + CARGO_TERM_COLOR: never + +jobs: + # Tells Gitea a result is on its way, so a status that is still missing + # reads as "running" rather than "never started". + pending: + if: vars.BUILD_ON == 'github' + runs-on: ubuntu-latest + steps: + - env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \ + "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \ + -d "$(jq -n --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + '{state:"pending", context:$c, target_url:$u, description:"GitHub Actions"}')" + + # The unit and mock tests. The #[ignore]d ones need live servers or + # containers and run by hand (README, "Testing"). + test: + if: vars.BUILD_ON == 'github' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-test-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: cargo-test-${{ runner.os }}-${{ runner.arch }}- + # rustup is on the runner; rust-toolchain.toml picks the toolchain. + - run: rustup show active-toolchain || rustup toolchain install + - run: cargo test --locked + + # Guards shared by both architectures, checked once. + version: + if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + runs-on: ubuntu-latest + env: + TAG: ${{ github.ref_name }} + steps: + # Full history: the ancestry check cannot be answered from a shallow + # clone. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + # A release can never describe code that was not reviewed onto main, + # and its tag must be the version the binary reports. + - run: | + git fetch --quiet origin main + git merge-base --is-ancestor "$(git rev-parse "${TAG}^{commit}")" origin/main \ + || { echo "::error::$TAG is not on main"; exit 1; } + want="v$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')" + [ "$TAG" = "$want" ] || { echo "::error::$TAG does not match Cargo.toml ($want)"; exit 1; } + + build: + if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + needs: [test, version] + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-22.04-arm + env: + TAG: ${{ github.ref_name }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - run: rustup show active-toolchain || rustup toolchain install + - run: | + SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "$TAG")" scripts/build-release.sh "$TAG" dist + sha256sum dist/*.tar.gz + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: archive-${{ matrix.arch }} + path: dist/*.tar.gz + retention-days: 7 + overwrite: true + if-no-files-found: error + + release: + if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + needs: [build] + runs-on: ubuntu-latest + env: + TAG: ${{ github.ref_name }} + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: dist + pattern: archive-* + merge-multiple: true + - run: | + (cd dist && sha256sum ./*.tar.gz | sed 's| \./| |' > SHA256SUMS) + cat dist/SHA256SUMS + - env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + set -euo pipefail + API="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY" + auth=(-H "Authorization: token $GITEA_TOKEN") + # Reuse the Release if the tag already has one (a re-run), else make a + # draft of our own. + created=0 + id="$(curl -fsS "${auth[@]}" "$API/releases/tags/$TAG" 2>/dev/null | jq -r '.id // empty' || true)" + if [ -z "$id" ]; then + id="$(curl -fsS "${auth[@]}" -H 'Content-Type: application/json' \ + -d "$(jq -n --arg t "$TAG" '{tag_name:$t, name:$t, draft:true, + body:"Binaries for linux/amd64 and linux/arm64. Verify with SHA256SUMS."}')" \ + "$API/releases" | jq -r '.id // empty')" + [ -n "$id" ] || { echo "::error::could not create the release"; exit 1; } + created=1 + fi + have="$(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r '.[].name')" + for f in dist/*; do + n="$(basename "$f")" + if grep -qxF "$n" <<<"$have"; then echo "already attached: $n"; continue; fi + echo "uploading $n" + curl -fsS -o /dev/null "${auth[@]}" --form "attachment=@$f" "$API/releases/$id/assets?name=$n" || { + [ "$created" = 1 ] && curl -sS -o /dev/null "${auth[@]}" -X DELETE "$API/releases/$id" + exit 1 + } + done + if [ "$created" = 1 ]; then + curl -fsS -o /dev/null "${auth[@]}" -H 'Content-Type: application/json' -X PATCH \ + -d '{"draft":false}' "$API/releases/$id" + fi + + # ---------------------------------------------------- github release ------ + # Copies this tag's Gitea release -- notes and files -- to a GitHub release, + # so the replica's Releases page, and anyone watching it, keeps up. Gitea's + # release is the real one; this is left out of the report to Gitea, so a + # failure here cannot fail a release. PR and issue numbers in the notes are + # rewritten to Gitea links: on GitHub a bare #16 is some other PR. + github-release: + if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'tag' }} + needs: [release] + runs-on: ubuntu-latest + permissions: + contents: write + env: + GITEA_URL: ${{ vars.GITEA_URL }} + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + steps: + - run: | + set -euo pipefail + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "GitHub already has a release for $TAG"; exit 0 + fi + # The Gitea release exists by now if this run made it; allow a few + # minutes either way. + code=0 + for _ in $(seq 1 15); do + code="$(curl -sS -o rel.json -w '%{http_code}' "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/tags/$TAG")" + [ "$code" = 200 ] && break + sleep 20 + done + if [ "$code" != 200 ]; then echo "No Gitea release for $TAG; nothing to copy"; exit 0; fi + if [ "$(jq -r .draft rel.json)" = true ]; then echo "The Gitea release is a draft; not copying"; exit 0; fi + export BASE="$(jq -r '.html_url | sub("/releases/tag/.*$"; "")' rel.json)" + jq -r '.body // ""' rel.json | perl -pe 's{(? notes.md + printf '\n\n_Mirrored from [the Gitea release](%s); report issues on [Gitea](%s/issues)._\n' \ + "$(jq -r .html_url rel.json)" "$BASE" >> notes.md + files=() + mkdir -p files + while IFS=$'\t' read -r name url; do + curl -fsSL -o "files/$name" "$url"; files+=("files/$name") + done < <(jq -r '.assets[]? | [.name, .browser_download_url] | @tsv' rel.json) + title="$(jq -r '.name // ""' rel.json)"; [ -n "$title" ] || title="$TAG" + if [ "$(jq -r .prerelease rel.json)" = true ]; then kind=--prerelease; else kind=--latest; fi + gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --title "$title" \ + --notes-file notes.md "$kind" "${files[@]}" + echo "created the GitHub release for $TAG with ${#files[@]} file(s)" + + report: + if: always() && vars.BUILD_ON == 'github' + needs: [pending, test, version, build, release] + runs-on: ubuntu-latest + steps: + - env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + STATE: ${{ contains(needs.*.result, 'failure') && 'failure' || (contains(needs.*.result, 'cancelled') && 'cancelled' || 'success') }} + run: | + # A cancelled run was superseded by a newer run for the same commit (the + # mirror can push one commit twice); that run reports. Posting "failure" + # here would fail the Gitea check while the real build is still going. + if [ "$STATE" = cancelled ]; then echo "cancelled: leaving the result to the newer run"; exit 0; fi + curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \ + "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \ + -d "$(jq -n --arg s "$STATE" --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + '{state:$s, context:$c, target_url:$u, description:"GitHub Actions"}')" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 55f6aec..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,342 +0,0 @@ -name: Release -permissions: - "contents": "write" - -on: - pull_request: - push: - tags: - - '**[0-9]+.[0-9]+.[0-9]+*' - -jobs: - # Run 'dist plan' (or host) to determine what tasks we need to do - plan: - runs-on: "ubuntu-22.04" - outputs: - val: ${{ steps.plan.outputs.manifest }} - tag: ${{ !github.event.pull_request && github.ref_name || '' }} - tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} - publishing: ${{ !github.event.pull_request }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - submodules: recursive - - name: Install dist - shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh" - - name: Cache dist - uses: actions/upload-artifact@v7 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/dist - - id: plan - run: | - dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "dist ran successfully" - cat plan-dist-manifest.json - echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v7 - with: - name: artifacts-plan-dist-manifest - path: plan-dist-manifest.json - - build-local-artifacts: - name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) - needs: - - plan - if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} - runs-on: ${{ matrix.runner }} - container: ${{ matrix.container && matrix.container.image || null }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json - AWS_LC_SYS_PREBUILT_NASM: 1 - permissions: - "attestations": "write" - "contents": "read" - "id-token": "write" - steps: - - name: enable windows longpaths - run: | - git config --global core.longpaths true - - uses: actions/checkout@v6 - with: - persist-credentials: false - submodules: recursive - - name: Install Rust non-interactively if not already installed - if: ${{ matrix.container }} - run: | - if ! command -v cargo > /dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - fi - - name: Install dist - run: ${{ matrix.install_dist.run }} - - name: Fetch local artifacts - uses: actions/download-artifact@v8 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - name: Install dependencies - run: | - ${{ matrix.packages_install }} - - name: Update Rust toolchain to current stable - shell: bash - run: | - if command -v rustup > /dev/null 2>&1; then - rustup update stable - rustup default stable - fi - - name: Build artifacts - run: | - # Actually do builds and make zips and whatnot - dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "dist ran successfully" - - name: Attest - uses: actions/attest-build-provenance@v4 - with: - subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*" - - id: cargo-dist - name: Post-build - shell: bash - run: | - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@v7 - with: - name: artifacts-build-local-${{ join(matrix.targets, '_') }} - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} - - build-global-artifacts: - needs: - - plan - - build-local-artifacts - runs-on: "ubuntu-22.04" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@v8 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist - - name: Fetch local artifacts - uses: actions/download-artifact@v8 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - id: cargo-dist - shell: bash - run: | - dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "dist ran successfully" - - # Parse out what we just built and upload it to scratch storage - echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - cp dist-manifest.json "$BUILD_MANIFEST_NAME" - - name: "Upload artifacts" - uses: actions/upload-artifact@v7 - with: - name: artifacts-build-global - path: | - ${{ steps.cargo-dist.outputs.paths }} - ${{ env.BUILD_MANIFEST_NAME }} - - host: - needs: - - plan - - build-local-artifacts - - build-global-artifacts - if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-22.04" - outputs: - val: ${{ steps.host.outputs.manifest }} - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - submodules: recursive - - name: Install cached dist - uses: actions/download-artifact@v8 - with: - name: cargo-dist-cache - path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/dist - - name: Fetch artifacts - uses: actions/download-artifact@v8 - with: - pattern: artifacts-* - path: target/distrib/ - merge-multiple: true - - id: host - shell: bash - run: | - dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json - echo "artifacts uploaded and released successfully" - cat dist-manifest.json - echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v7 - with: - name: artifacts-dist-manifest - path: dist-manifest.json - - name: "Download GitHub Artifacts" - uses: actions/download-artifact@v8 - with: - pattern: artifacts-* - path: artifacts - merge-multiple: true - - name: Cleanup - run: | - # Remove the granular manifests - rm -f artifacts/*-dist-manifest.json - - name: Create GitHub Release - env: - PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" - RELEASE_COMMIT: "${{ github.sha }}" - run: | - awk '/^## \[/{c++} c==1' CHANGELOG.md > $RUNNER_TEMP/notes.txt - - # Tag-push releases are created as drafts; the `announce` job un-drafts - # them only after the build succeeds, so watcher notifications don't - # fire on broken builds. - gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --draft --title "${{ needs.plan.outputs.tag }}" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* - - publish-homebrew-formula: - needs: - - plan - - host - runs-on: "ubuntu-22.04" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PLAN: ${{ needs.plan.outputs.val }} - GITHUB_USER: "axo bot" - GITHUB_EMAIL: "admin+bot@axo.dev" - if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: true - repository: "stalwartlabs/homebrew-tap" - token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - - name: Fetch homebrew formulae - uses: actions/download-artifact@v8 - with: - pattern: artifacts-* - path: Formula/ - merge-multiple: true - - name: Commit formula files - run: | - git config --global user.name "${GITHUB_USER}" - git config --global user.email "${GITHUB_EMAIL}" - - for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do - filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output) - name=$(echo "$filename" | sed "s/\.rb$//") - version=$(echo "$release" | jq .app_version --raw-output) - - export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH" - brew update - # We avoid reformatting user-provided data such as the app description and homepage. - brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true - - git add "Formula/${filename}" - git commit -m "${name} ${version}" - done - git push - - publish-npm: - needs: - - plan - - host - runs-on: "ubuntu-22.04" - if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} - permissions: - "contents": "read" - "id-token": "write" - steps: - - uses: actions/setup-node@v4 - with: - node-version: '22' - - name: Fetch npm tarball - uses: actions/download-artifact@v8 - with: - pattern: artifacts-* - path: artifacts/ - merge-multiple: true - - name: Upgrade npm for trusted publishing - run: npm install -g npm@latest - - name: Publish to npm - env: - IS_PRERELEASE: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }} - run: | - TAG_FLAG="" - if [ "$IS_PRERELEASE" = "true" ]; then - TAG_FLAG="--tag next" - fi - npm publish ./artifacts/vandelay-npm-package.tar.gz --provenance --access public $TAG_FLAG - - announce: - needs: - - plan - - host - - publish-homebrew-formula - - publish-npm - if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') }} - runs-on: "ubuntu-22.04" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - submodules: recursive - - name: Un-draft release - run: gh release edit "${{ needs.plan.outputs.tag }}" --draft=false --repo "${{ github.repository }}" - - cleanup: - name: Cleanup failed release - needs: - - plan - - build-local-artifacts - - build-global-artifacts - - host - - publish-homebrew-formula - - publish-npm - - announce - if: ${{ failure() && needs.plan.outputs.publishing == 'true' }} - runs-on: "ubuntu-22.04" - permissions: - contents: write - steps: - - name: Delete draft release and tag - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release delete "${{ needs.plan.outputs.tag }}" --yes --cleanup-tag --repo "${{ github.repository }}" || true diff --git a/assets/importer-exporter.jpg b/assets/importer-exporter.jpg deleted file mode 100644 index 1fa96d2a6fffe9312d8f661ae4148a732f788af1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7754 zcmb7pbx;&;(Eq{FNJ=P2AL$X&-Q5C*(ny0K-Q5k+4N^xphj1Vz-Q9=M937&B2(RCF z=KbUM=kK$#GtbV>?z1to&&;z=D^HsMVpT;IMF1Ka0D$(708i_H2mme?HtBOx^8c8e zobm+)B?Top<-Zd-CFP43l#~?Y-3AzWNslyrQ;d>le-oLmqz zJUl!?0zxWcVk%A=8hXzEZ#?w^NN@n((11WRMgTeq8ju9-X#fBM0MLPG|AqR0z`#TY zVgb;w|0yMi0cb#U40Id}Obm21%>T^?9f(1KNy@+@qmA{P(cCpOrHQw=Zhioag>osM~f zXjNEjn@=&4um&Qlo-J|@vDJ(A3eZvd>=)7|3}|4gL=A1}qi-)dlMp0To%Q~ca#V`8Qd{&4#Dws>lswy#IS)>c$agx0 z%6|5SXSNSUC#l=XPk>QyUQqnIaD_XP$C*rgM|%mfAgVF0NInnMHuGw_Il)ry!FQOmFAd2{&1cuZ)QsDM8zn^e zClVal(2=D?)f%z^5WbW0gcqF=$7Xdv{Y)PFG-Hf;;)n-Q8DHN_n!JL{u639e=YuHP zAT^Tx)2xiGW9Pjq1%oZLwSS8f_?Po7km$kZffJ=Vz5N23enzdY8J)7_@dPNT9XDs@Iot&8y(bV?++fhf%FNzVe-bB) zs#Eq{%rJ^FVbrNmpPez@^iDtg2Sx%Ds=$rBc}Oj;hKTADhWj^}b;@qecZ^I__3{)< zg*rEsV(wLXk)?-Nc!r{=q39i8^c~?|Ve=&5_iN2~wb^!#Z|4g^8czVLBGhXG>KQF7 z8Wpcc8GE*>+#!Lm{r+H3Iw_H&dzm^{IsmtY%Rh2*OgjtS9X25XYmxF6iR@`D?({8q zs~U~Zo4?#*gg8Jx*W)MD-k|ZdmbSx)4kPH2k+j@z{n@KLw~X{jd~?13LLZmY`T-dP z(>Y6)6`TmDJIk3NwI;uQu1shP7fMFx>ApXuDf#JIA41UD!4(h$3if`K@~Cg1!mKB- ze;HY?{2Rx_ML>{yu!(Y#!Bq&Tc-ofgJ3I8O?54KTu<%pI#82(dn3q@f3^*vGLGCbU zfG#^2b^%)TSEJ>^8`4v}SLx4dAP#S&4)!Mhj(uAnd`>;zb`Aeca`u zrU0LS)rATgd~*6riQ&@MctZ$3?EGnbsG1&3K0;c@u*P4ut4}%t9Sb$1rw&N6)vC7I z46C=v>Cu=Tg~-T?YZTNqcx9ZC)9ndOZ*WvsrkqV^v;%|*RleZXnp!Jz?dly;QoJ%9 zaLBFn*KLxvvY7M;?JB<|ul1*JE}Lk{*@W&!$8do<-fz4Swp*l8_og|LYmR+cz2_0G zHxkDm)h6cn7xPP3WZl4}c5y{HkPpMMd*`^B6zCAlB^>#=CLE0XuC(AHnW;FDQrs}C zzup?o^Ve)m5`|~%q?l^5-Xp=eWmP@yJsZH1q?~&oe;_P)B{BF-uu$#|Q7XHyD1K$B zlwP7|f;(>}(rKBlFgHT-x!EE-`lIEI=4AqgSY2geT^LqdlZ!J0h@vfWY}#VQVe8)` zn2aB@z+WJz^7rjRB#a^~7m{+8oI|)w&;|*Uri~PYp}N%am$Ju!p`J)-fStgXq9gv+ z#V(?w0q0c3eUh$*gNuZ38}}spttYirDT9~VoshxUT+IfJp*I_bTq@fd^NMUrg4Q*D zbYO7CU4J~6w4GpprIJ|tk7!o|KG)6eR{p4hE^8ANp~c2Q8@Q1TJV29H5dYxC!lcKX zxIi&%u$Mm?EzpC(evp3f3uFRrLYvlPMNJ~89PJtIh}qxT!Hwl4jhfng0?h5D!Kd2$ z1}jcx=E)qCsg~BArYv8AaC=Q^>k@7P7p6PAXH0v3lT`Rw$v-o20>Yma}4jdf3aO zd&j?$rDR;!ZoaEJIzrPSM2`q>A{+8;(xcSNw@&Wc#C6e!X#w^NabK)r>%`8Cg@Z2v ziz#hzDLcIh-qHP%H(xZCxtTWn8evV2#6AO5xPaZ|;9pW&^a;_{E_M=WO*6N2dk-m! ze4HDBy673rGQ?Kala^GtE-h*ylw`{t$6a{_Yd4-(uH?qekF{`+zJLube)iQD6#w)2I(Df@PN zc`orXg$=^DcND9+UPSMOPR(|3k^6Tn2tGj;%kCk+UT`t7-wPexvM+U zcn9Iibf2L%6erCs3;gu@gUutY5W$qi+0X2FUjyS~)BESGxA$c}A04)sjQDMJ=6iqI zZ|c4h%Ho-$K|XZ#x44k;D!9dMrKx5w#kH`%v-l++LdDN?yW)DbnW;qIU6r3m=%Dt| zg})=Un=oFXT|l>aZ$t^){!3m^ynaTX&V!6lum#rRt`KrX^%?3ZAsJeWwHw0iUNvu2 zQ3JJiu&B;08J*hR`T~~aNBTt^5Dq!bF0FvpU$0yss7W<5`30*coquBq%?Z*!8+c9f zuwlsO^3hL(8B-u=x_DLT)sRer>IAGo5Nx?dOLb!)RCY)&1rlHXSMa~fP&XFWF-}oh z;*z~n{*z>rB#eOJ7$mzyby&PZ^?j$A<#yV19RIOJkQsfUg?6;!rhnIx^F`ar^Bzf} zw&wJ+F~yLYWA_wu2cM$+$K7f22z-5`V2|3yt2bH9dT;(I|E!Td^ISt4Q~IlG75!}z`>JH+k>QVQ z661!9X|E?898$CHb<1TM_nVL|@E!d`rg3XjR1*Zfp1b6#Bp_UfyS(Rk(u%EtEcvAs z_?H;6h}(11bCmVY^RzbGeF?J2nU?zd7^5)*jU|o{vXbY)fjQ|aO0dw|9OVR*dD!qF z9fKy)rIjv&D!ORg=?LVV1Y2@9eM!Yw7ptB#xaTT^UR|u!*DW)iijr&%ct@l_B%_^N zS5Q7lSPx3CYfJ0Kyswm+W19hU9GqUlZft%DxTj02(Owm^kYO%u1>r6i-f+@r|4$NU zIp#ia$9(K)Ldv;G*6cg)4;A4aZv~YK6(7y12*yQ>QVg7up`J{Gv<+ToV(>zMi#P3Y z4Vx-Ci>d(_6339B=h>o0zs!eJiX3qs!4MzV#d$k61i-%y<|;dp6*CeVsgz1JxyqvYNsrjsh7)wpp9>jb zCk{(qG?J4VWS!G)y)LWd@UqEs#qh3?)gUQ#h)L?ZI^T}?$i4&q3LwyvOTeY#XYSty z(A?d!D<W|#t>#&yg*OJIFpM|Asotav5%UPfqvArp%zq`S(uRGK-5J+1;_9xW+YQXL7`-OHbupz9{}Yx3&Zv+^&tn(P z@(Y~FAcm8AUkWMjIKCb!sc9KAJirD>l1htuH&~TRl$Iw+?kSR3h_&9A*P&0%-7Gtu zx62wDQW1<9QD3W8AwZzSah1lixwX5L-L`BfJ$yuZ$NVFJx^sEBvO2*H8-Il^V|jJ6 zAPTTgY^cx&(*OL&Bv^Ca2vz7aKf|pIS;x>yzM!z%zX@!+g6wMf`u}|H54uftlqwjP znaej`P?r`;%xVi3;`mE6qy!Gmj5A<8;Pepv6al2c*BU^(`n_A3nNb`<;zC#$fX7y# z$Jf0zNsZlxQ6X|Fu0a)oor*Y6Fp?yf#0jA)^TN4&r=8c@ji&oX3{y2_&DY@lHE-`x zxJ}baJYBJ&p>NO6(j_PuxD5-sbRbSqRhM4F1!^Hs79rONYIZb}T#oVxPAdB%B# z?3zqhFOmXghq9W%fEWS%%JF`sR781y z{sP!nO3uO9`vky)HF=FYKQ#9$s8ef0 z8wBqHgQ`oh@h=-6{rqYwSF12N+98P#$(;PY`TOP1>T^=E!;wogYR<(-Usvc*@2bY_ ztv{y6&(|(%4`K@s1t^qRYWwP{*bPdHxHy`?)c+J!z80@|V3aj0M--T?~o@t?D4rHTaLMsT1;S)y4J*PIox(!Sq$z6J8U^|U5?>^e z>xy`M#j9&pV?ha~|BjAc6lJzW6;uak#%GYfzbbh5$|f6K29k`ngB8cyOVh6PWA;X$ zLYPKSl#Hw$I{975*Wn~QA~_(XurMUis8Y^KJZ_~gbmIJUOlo9(ufv>DA@A?`mEC>% z7%4DUkpS-8dO{Q`udCfvPw1`;jN3>`RGc}*P7oyN1(^Cm^y1`r}7#Qs*x?+rs6R;0XUXTJaRsu>;OaZ%L|*b=(Jxw)j7 zNnq_)bLQ_97w0#$aRlihT6{NBvb9!AXFXDlx_KznAkC_voUivL%t)*IYtUFWYiEl? zLsoM}K(22R>p8NblPhKd5QkLx-JBr|52`fa&U^xB?kWI(zD_dJ)2F^`$APUtYAa!) zjCjQpci6SJCYxQQ5-FR-#{R5*{?h(vmL6X8!JOaeptLXiSqJHK#JFgNSAlY+TuoWn zO%K+5C7DyWAB7h(7B%O;a@u^D48Rf-mK6M&r{CVuU(mb3HCWlA=!O-Bb23EZCODt1 zw?&Rvpk_*|@Ok68JAhDH7mC^ho zrc-_sUk@zRz+711Z~weMo_h6;K~WNcJL7}t!iJOYD6lCEG1NMe;%-JYlfA%8W|RJ3 zA(=HtzAvP9SiRz9sjYkL&v}&Y>~^5)kVm=ELeR|YRnH>F_prFM|9c)QrP2V>D z6ju@yY}Y60S~dW$Wdvmjk5ZaglIJ97tTbkfkuEE(Z$jG*eR7*mF8~QGF(RpH^H}X+~OIYgZquSjMfY zBCI`DI5_B2=Q9iax^H~ZKnZhOvOG>6E?N%R)GOhc-i?l2BLdXh>k%VrssDB#nJ4ZC zsTM&)MH3ACr^NUyOy^W!IcZQ+#xQ%8y6*!u9u23Axi)8~1X&wbD3b+#^{`7A7rcyT^1N1^Yfc6GD(8Y~-%1H6v0N$39JEr2AU zKkvW;+;)~2=gq4gzTr;*J)g!lqnJbUkMuQP;*|tR$mNBtBKQR}3v(8|?1W&RQrmnz zQ-xk;`JuQFIm=Wn+o>ZAKa>6^0DXmyCHii>QSQBsfZ+=V7&&JNS5?Vr%(I(mXm&1J|BIs|8jwPnJGC(y`T=LF>g9=> z^2KjGIsLVqJSeJ%jT zWyOeJTgcg{$c?CrW|A+&hWrczM6#Q?^%JXOAT-Ek{7|3;FAlfmof33^ToEI+?^auN zmHNV(o1-)P4^}dF=CbT{;AClW!BfaKHbuKVHGt+dPs3hUB}e zupa?tQa<`LlzVu2>5c2rXh`QKw0_F0o3s{*oFs76f#b&@jZG-b&ZsUJ%#@5Ebn!W+ z6B092!j_D*2>7MIm7pL`Xj!{RnO9xC1F9)i^1}zf%X+dy!0jm2O+1mn^-CUMwO2J& zF@fsD+5;cd2@;TkH2VnIfCd(k>=L zulr82Mf%00c$(2N*Ppd*7*4pAsB7Vdo)gCDvz4bHz-U*ZLL$>lf;UV+^AZkx^*fIC z_s#M@Kik z5sh(zz5jRVXA%|dh_h4vBNb*x88g8en|Fw>v9ys)!!c0d4K-E)j)K$d{Ym=Pjl69_ zim^>!l8=J2mhFoQy5ZF<9+5?@ZDUJ~H2wNBRO^?Gi*F8!H>#8S4K8z&x>{vzAL%LD z)yf^Bq6jH&r9aj+Gdk)NN3rKYA{tv0W4C*oYBB2^968_PMaO^5Sq0d`3@_UiSR zS5F&Lb8~xYS5gzz;Vz-WE*GIY?WWei?dl@Wy3@q*0?(&j@h{7VkEs;e9`u>x?Zhr(L0N^k3I)B~qI25Lq~T<7jp z*-^sFY3M<8roMhx5nL==M1_iB-?fL<7)qNl<0lFzJxAz_84o+r~JoO#%pj|GiY3ENBG7`PV93f zK{0J{+q#@{B7fseM_~Tap1q45W#f97>Vs4D8;ewLM7^VSJIE^!jfEx6As;q0>}f;& z*hd=_YlJx})JBFL-R{N8DK?O-jQWn|WV5~{*4m2{DesrXnQ_CrPfF2K;bRSy+u(Q~ zCDqJ}jlPT}P$bYsNWX#)Yl8B})7({!5)n57FYdDhk`n6~K8+TXEO|rKa)Q^4&vjBe zsVk35^HsN)I2{Pp#xeNlcl(@w4W#_L#`Z>8duJ&`+?Tm@UUU$}#*BT6xaBf#%F&Rz z-q_T;eh{bRQsG_k=l1F|;rcP9mc`Ed4bjbv&1GDGGvdOA6xx!Ml18|dFi2wA-Upep z)~xu}8|NpenEQWT3yO;!8qcDS79SG;3)$@hn*KF!eM4}&gagHJ_v1> zSG%Cn3-~t`2bHP6${6=W#|k6e9U0>(0YoSqer!7t*stJ4pEKI=#qlIF-?{ESD>)_K eI^AFxT~_lix8{?0)WpcFd(poQCh+sq>i+ +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# Build one release archive for the machine this runs on. +# +# Usage: scripts/build-release.sh VERSION OUTDIR +# scripts/build-release.sh v2026.9.30 dist +# +# CI runs exactly this on one native runner per architecture (amd64 and +# arm64) and joins the results with SHA256SUMS, so a release can be checked +# before tagging on any Linux machine with cargo. The archive name carries no +# version, so .../releases/latest/download/ always means the newest. +set -euo pipefail + +VERSION="${1:?usage: $0 VERSION OUTDIR}" +OUT="${2:?usage: $0 VERSION OUTDIR}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64 | arm64) arch=arm64 ;; + *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +# The tag names the version the binary reports, so the two cannot disagree. +want="v$(cd "$ROOT" && cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')" +[ "$VERSION" = "$want" ] || { echo "tag $VERSION does not match Cargo.toml ($want)" >&2; exit 1; } + +name="inbuxa-migrate-linux-$arch" +mkdir -p "$OUT" +OUT="$(cd "$OUT" && pwd)" +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT + +echo "==> building $name ($VERSION)" +(cd "$ROOT" && cargo build --release --locked) +mkdir -p "$STAGE/$name" +cp "$ROOT/target/release/inbuxa-migrate" "$ROOT/README.md" "$STAGE/$name/" +cp -r "$ROOT/LICENSES" "$STAGE/$name/" +# Fixed owner, order and time, so the archive's layout and metadata do not +# change from one build of a tag to the next. +tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH:-0}" \ + -C "$STAGE/$name" -czf "$OUT/$name.tar.gz" inbuxa-migrate LICENSES README.md +echo "==> $OUT/$name.tar.gz" diff --git a/wix/main.wxs b/wix/main.wxs deleted file mode 100644 index 1715d41..0000000 --- a/wix/main.wxs +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 1 - - - - - - - - - - - - - - - - - - From bfea4a54670dd93f3d90826bc0a46cdeb6aae512 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 10:15:07 -0700 Subject: [PATCH 4/6] Rewrite the README for inbuxa-migrate The README now says what the tool is in the suite's plain voice: the archive in the middle, convergent reruns, --dry-run, the archive as a backup, the sources it reads (Stalwart among the JMAP ones), the INBUXA_MIGRATE_* credential variables, installing from the Gitea release with SHA256SUMS, building and testing, and the license with the lineage line. Upstream's logo, badges and install channels are gone. The full command and flag reference moves to docs/usage.md, with the renamed binary and variables. The CHANGELOG gains a section for this first release, above upstream's history. --- CHANGELOG.md | 22 ++- README.md | 402 ++++++++++----------------------------------------- 2 files changed, 96 insertions(+), 328 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d51adc4..ff68edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ # Change Log -All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +All notable changes to this project are recorded here. Versions are dates: +release `v2026.9.30` is version `2026.9.30`. + +## [Unreleased] -- 2026.9.30 + +### Changed +- Renamed to inbuxa-migrate: the binary, the crate, and the credential + variables, now `INBUXA_MIGRATE_PASSWORD`, `INBUXA_MIGRATE_TOKEN`, + `INBUXA_MIGRATE_EWS_CLIENT_SECRET` and `INBUXA_MIGRATE_GRAPH_TOKEN`. The old + names are not read. Archives written by Vandelay are not read either. +- Registry calls (`x:Account`, `x:Domain` and the other `x:` types) use the + capability the server advertises: `urn:inbuxa:jmap:registry` on inbuxa, + `urn:stalwart:jmap` on a Stalwart source. A server that advertises neither + gets a clear error instead of a request it never offered to accept. +- Released as Linux archives for amd64 and arm64 on the Gitea release page, + with `SHA256SUMS`. The npm, Homebrew, shell and PowerShell installers and + the MSI are gone. + +--- + +# Earlier releases (upstream Vandelay) ## [1.0.11] - 2026-09-28 diff --git a/README.md b/README.md index 2b6c1e7..673a425 100644 --- a/README.md +++ b/README.md @@ -1,355 +1,103 @@ -

Vandelay

+# inbuxa-migrate -

- The JMAP importer-exporter -
- One-shot account migration and backup across JMAP, IMAP, CalDAV, CardDAV, WebDAV, ManageSieve, Maildir, Google Takeout and Microsoft Exchange. -

+> [!NOTE] +> Development happens on [git.coffeylabs.org/inbuxa/inbuxa-migrate](https://git.coffeylabs.org/inbuxa/inbuxa-migrate); the copy on GitHub is a read-only mirror. +> Report issues at **[git.coffeylabs.org/inbuxa/inbuxa-migrate/issues](https://git.coffeylabs.org/inbuxa/inbuxa-migrate/issues)**, and join discussions at **[community.coffeylabs.org](https://community.coffeylabs.org)**. -
+One program that moves an account into **inbuxa**: its mail, calendars, +contacts, identities, Sieve scripts and files. -

- build -   - release -   - npm -   - downloads -   - platforms -   - license -

+ inbuxa-migrate import imap … alice.sqlite an account into an archive + inbuxa-migrate inspect alice.sqlite what landed + inbuxa-migrate export … alice.sqlite the archive into inbuxa -
+Between the two sides sits an archive: one SQLite file holding one account. +`import` fills it from the old server; `export` writes it into the new one. +Import and export never talk to each other, only to the archive, so the old +server can be gone before the new one exists. - - - - - -
+Both commands converge. An interrupted run picks up where it stopped, and a +later `import` into the same archive adds what arrived since. Every command +takes `--dry-run` and then only reports the plan. `inspect` reads an archive +and changes nothing. -- Jerry: Well, what does **he** do? -- George: He's an importer. -- Jerry: Just imports, no exports? -- George: He's an **importer/exporter**, okay? +The archive is also a backup. Import on a schedule, keep the file, and +restore it later with `export`. An archive remembers which account filled it +and refuses a different one unless told otherwise. - -He's an importer-exporter. -
+Export speaks JMAP only. It writes into inbuxa, or into any JMAP server that +advertises the types being written. The full command reference is in +[docs/usage.md](docs/usage.md). -  +## Sources -## About +- **JMAP** -- any JMAP account, including an inbuxa or Stalwart server. +- **IMAP** -- mail only. Folders are chosen by name or by pattern. +- **CalDAV, CardDAV** -- calendars and events, address books and contacts. +- **WebDAV** -- a plain file collection, kept as a file tree. +- **ManageSieve** -- Sieve scripts only, with the active one recorded. +- **Maildir** -- a local Maildir++ tree. No network. +- **Google Takeout** -- the `.mbox`, `.ics` and `.vcf` files in a Takeout + export, or in any directory tree laid out that way. +- **Exchange Server** -- an on-premises mailbox, through EWS. +- **Exchange Online** -- through Microsoft Graph. EWS is being retired in + Exchange Online: blocked from 1 October 2026 unless an administrator allows + the client, and switched off on 1 April 2027. Public folders are only + reachable through EWS. -Vandelay is a one-shot account-migration utility for JMAP, the JMAP analogue of `imapsync` generalised to every JMAP data type (mail, contacts, calendars, identities, sieve scripts, file storage). It imports an account from a wide range of source protocols into a local SQLite "archive", then exports that archive into a target JMAP server. Import and export never talk to each other, only to the SQLite archive. One archive holds exactly one account. +## Credentials -Because the archive is a self-contained SQLite file that fully describes one account, vandelay doubles as a per-account backup tool: run an import on a schedule to capture a fresh snapshot, keep the resulting SQLite file as your backup, and restore it later by running an export against a JMAP target. +Secrets are read from the environment, or asked for at a prompt: -## Features + INBUXA_MIGRATE_PASSWORD a password for --auth-basic + INBUXA_MIGRATE_TOKEN a bearer token for --auth-bearer + INBUXA_MIGRATE_EWS_CLIENT_SECRET the OAuth client secret for app-only EWS + INBUXA_MIGRATE_GRAPH_TOKEN a Microsoft Graph access token -- **Many source protocols:** - - JMAP - - IMAP - - CalDAV - - CardDAV - - WebDAV - - ManageSieve - - Maildir++ - - Google Takeout - - Microsoft Exchange Server (on-premises) via EWS - - Microsoft Exchange Online via Graph -- **One target protocol:** JMAP, with type-by-type stateless re-matching on every run. -- **Convergent:** Re-running an interrupted import or export picks up where it left off without bookkeeping flags. -- **Multi-threaded, no async runtime:** Blocking HTTP with per-server concurrency caps respected automatically. -- **Content-addressed blobs:** Emails, sieve scripts and file-storage payloads are stored once by BLAKE3 hash and deduplicated across the archive. -- **Dry-run everywhere:** Every command supports `--dry-run` to compute the full plan without writing. -- **Source-change protection:** An archive remembers which account it was filled from; pointing it at a different one fails unless explicitly permitted. -- **Read-only inspection:** A built-in `inspect` command dumps any object type from an archive for verification. +The command line takes them too, but a secret there ends up in the shell's +history and in the process list. Use the environment or the prompt. -## Install +## Installing -```sh -# macOS / Linux -curl --proto '=https' --tlsv1.2 -LsSf \ - https://github.com/stalwartlabs/vandelay/releases/latest/download/vandelay-installer.sh | sh +Linux, on amd64 or arm64. Each release carries one archive per architecture +and a `SHA256SUMS` file: -# Homebrew -brew install stalwartlabs/tap/vandelay + base=https://git.coffeylabs.org/inbuxa/inbuxa-migrate/releases/latest/download + curl -fLO "$base/inbuxa-migrate-linux-amd64.tar.gz" + curl -fLO "$base/SHA256SUMS" + sha256sum --check --ignore-missing SHA256SUMS + tar -xzf inbuxa-migrate-linux-amd64.tar.gz + ./inbuxa-migrate --version -# Windows -powershell -ExecutionPolicy Bypass -c "irm https://github.com/stalwartlabs/vandelay/releases/latest/download/vandelay-installer.ps1 | iex" +On arm64, fetch `inbuxa-migrate-linux-arm64.tar.gz` instead. The binary is +the whole program; put it anywhere on the path. -# npm -npm install -g @stalwartlabs/vandelay +## Building and testing -# From source -cargo install --path . -``` + cargo build --release the binary is target/release/inbuxa-migrate + cargo test the default suite -A signed `.msi` is also published with each release. +The default suite needs no network and no Docker. Unit tests and scripted +mocks stand in for JMAP, DAV, EWS and Graph servers. -## Quick start +The live tests are marked `#[ignore]`. Each boots a throwaway container -- +Stalwart, Dovecot, Cyrus, Radicale, Baikal or Apache `mod_dav` -- so they +need a working Docker daemon. Run them one binary at a time, and always with +one test thread: -A typical run is two commands, one to capture a source account into a local SQLite archive and one to push that archive into a JMAP target. + cargo test --test sync_jmap -- --ignored --test-threads=1 + cargo test --test integration_dovecot -- --ignored --test-threads=1 -```sh -# 1. Import an IMAP mailbox into a fresh archive. -export VANDELAY_PASSWORD='source-app-password' -vandelay import imap \ - --url imaps://imap.example.com \ - --auth-basic alice@example.com \ - alice.sqlite - -# 2. Peek at what landed. -vandelay inspect alice.sqlite # per-type summary -vandelay inspect alice.sqlite mailbox # mailbox tree -vandelay inspect alice.sqlite email --limit 5 - -# 3. Push the archive into a target JMAP server. -export VANDELAY_PASSWORD='target-password' -vandelay export \ - --url https://jmap.example.org \ - --auth-basic alice@example.org \ - --account-name alice@example.org \ - alice.sqlite -``` - -Both commands are convergent: rerun either to resume an interrupted run, or rerun `import` later to pick up new mail since the last snapshot. Use `--dry-run` on either side to compute the full plan without writing. - -## CLI quick reference - -``` -vandelay [args...] -``` - -All actions accept a set of global flags (verbosity, worker pool size, retry policy, TLS handling) in addition to action-specific ones. The most useful are: - -| Flag | Purpose | -| --- | --- | -| `-j, --threads ` | Worker pool size (default: logical CPUs). | -| `--dry-run` | Compute the full plan; perform no writes. | -| `-v`, `-vv`, `-vvv` | Increase log verbosity. | -| `-q, --quiet` | Warnings and errors only. | -| `--max-retries ` | Max retries per request on transient failures (default 5). | -| `--allow-invalid-certs` | Accept self-signed / invalid TLS certs. | - -Credentials should be supplied via the `VANDELAY_PASSWORD` / `VANDELAY_TOKEN` / `VANDELAY_EWS_CLIENT_SECRET` / `VANDELAY_GRAPH_TOKEN` environment variables, or via an interactive prompt; passing them on the command line is supported but not recommended. - -### Import - -``` -vandelay import [source-args...] -``` - -Reads a source account into the local SQLite `ARCHIVE` (created if absent). Every importer accepts `--allow-source-change` to override the archive's source-identity guard. - -#### JMAP - -``` -vandelay import jmap \ - --url \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ - (--account-id | --account-name ) \ - [--objects ] \ - -``` - -Imports a single JMAP account. `--objects` accepts a comma-separated list of object tokens (`mailbox,email,calendar,calendarevent,addressbook,contactcard,identity,sievescript,participantidentity,filenode`); default is everything the server advertises. - -#### IMAP - -``` -vandelay import imap \ - --url imap(s)://host[:port] \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN] --auth-user ) \ - [--include ...] [--exclude ...] [--exclude-special ...] \ - [--folder ...] [--subscribed-only] [--noautomap] \ - [--include-deleted] [--allow-cleartext] [--compress] \ - [--fetch-batch ] [--imap-connections <1..8>] \ - -``` - -Imports mail (and only mail) from any IMAP server. Folder selection is via `--include`/`--exclude` regexes (mutually exclusive with the exact-match `--folder`); `--exclude-special` drops by SPECIAL-USE role. - -#### CalDAV - -``` -vandelay import caldav \ - --url \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ - [--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch ] \ - -``` - -Discovers the user's CalDAV principal (or accepts a URL pointing straight at a calendar-home or calendar), then imports calendars and events. - -#### CardDAV - -``` -vandelay import carddav \ - --url \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ - [--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch ] \ - -``` - -Same shape as `caldav`, but for address books and contacts. - -#### WebDAV - -``` -vandelay import webdav \ - --url \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ - [--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch ] \ - -``` - -Imports a plain WebDAV file collection as a JMAP `FileNode` tree. - -#### ManageSieve - -``` -vandelay import managesieve \ - --url sieve(s)://host[:port] \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN] --auth-user ) \ - [--allow-cleartext] \ - -``` - -Imports sieve scripts only. Each script is content-addressed in the blob table; the active script is recorded. - -#### Maildir - -``` -vandelay import maildir \ - [--include ...] [--exclude ...] [--folder ...] \ - [--noautomap] [--include-deleted] -``` - -Reads a local Maildir++ tree (a directory with `cur/`, `new/`, `tmp/`). No network. Folder selection mirrors the IMAP importer. - -#### Google Takeout - -``` -vandelay import takeout [--noautomap] -``` - -Scans a directory tree recursively for `.mbox`, `.ics` and `.vcf` files and imports them. Tailored to Google Takeout layouts but works on any such tree; system-label role assignment can be disabled with `--noautomap`. - -#### Microsoft Exchange (EWS) - -``` -vandelay import exchange-ews \ - [--url ] [--mailbox ] \ - [--mailbox-kind primary|archive|public-folders] \ - (--auth-basic [--auth-password ] \ - | --auth-bearer [TOKEN] [--ews-tenant --ews-client-id \ - (--ews-device-code | --ews-client-secret )]) \ - [--ews-connections <1..8>] [--ews-getitem-batch ] [--ews-attachment-batch ] \ - [--ews-no-syncfolderitems] \ - -``` - -Imports a mailbox via EWS from an on-premises Exchange Server. Autodiscover is used when `--url` is omitted (a `--mailbox` SMTP address is then required). Supports Basic, pre-acquired bearer, interactive device-code OAuth, and app-only client-credentials OAuth. - -For Exchange Online, use `exchange-graph` instead. Microsoft is retiring EWS in Exchange Online: from 1 October 2026 it is blocked unless a tenant administrator sets `EwsEnabled` to `True` and adds the client id to `EwsAllowedAppIDs`, and on 1 April 2027 it is switched off for every tenant. On-premises Exchange Server is not affected. - -#### Microsoft Exchange (Graph) - -``` -vandelay import exchange-graph \ - (--client-id [--tenant ] | --access-token [TOKEN]) \ - [--user ] \ - [--mailbox-kind primary|archive] \ - [--objects mail,calendar,contacts] \ - [--event-body-format text|html] \ - [--graph-connections <1..16>] [--top <1..1000>] \ - -``` - -Imports a mailbox from Exchange Online via Microsoft Graph. Without `--access-token`, the interactive device-code flow is used. `public-folders` is rejected here: Graph does not expose public folders, so they can only be imported with `exchange-ews`, which for Exchange Online is subject to the retirement described above. - -### Export - -``` -vandelay export \ - --url \ - (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ - (--account-id | --account-name ) \ - [--objects ] [--prune [--yes]] \ - -``` - -Stateless re-export of `ARCHIVE` into a target JMAP server account. The default behaviour is upsert-only: matched items are updated, unmatched local items are created, but pre-existing target items not covered by the archive are left alone. - -`--prune` enables destructive reconciliation: target objects that do not match anything in the archive are deleted. The confirmation prompt can be skipped with `--yes` for automation. Export speaks JMAP only; no other target protocols are currently supported. - -### Inspect - -``` -vandelay inspect [TYPE] [--limit ] [--offset ] -``` - -Read-only dump of a local archive. This command never opens a network connection and never writes to the archive. - -- Omit `TYPE` for a per-type summary (counts of every object kind plus blob storage stats). -- Pass an object type to dump it: `mailbox`, `email`, `identity`, `sievescript`, `addressbook`, `contactcard`, `calendar`, `calendarevent`, `participantidentity`, `filenode`. -- `mailbox` and `filenode` render as a tree (`--limit`/`--offset` are ignored); all other types use a paginated list and respect `--limit` and `--offset`. - -## Testing - -The default suite is hermetic (unit tests plus `mockito`-scripted JMAP/DAV/EWS/Graph behaviours) and needs no network or Docker: - -```sh -cargo build -cargo clippy --all-targets -cargo test -``` - -### Live and integration tests (Docker required) - -Live integration tests against a Stalwart server and container-based tests against third-party servers (Dovecot, Cyrus, Radicale, Baikal, Apache `mod_dav`) are gated behind `--ignored`. **They require a running Docker daemon**: each test binary boots its own throwaway container via `testcontainers` (images are pulled automatically on first run), so Docker must be installed and `docker info` must succeed before invoking them. - -Run them per binary, and always with `--test-threads=1`: - -```sh -cargo test --test sync_jmap -- --ignored --test-threads=1 # live JMAP import/export/convergence/prune -cargo test --test sync_imap -- --ignored --test-threads=1 -cargo test --test sync_managesieve -- --ignored --test-threads=1 -cargo test --test sync_maildir -- --ignored --test-threads=1 -cargo test --test sync_caldav -- --ignored --test-threads=1 -cargo test --test sync_carddav -- --ignored --test-threads=1 -cargo test --test sync_webdav -- --ignored --test-threads=1 -cargo test --test live_stalwart -- --ignored --test-threads=1 -cargo test --test seed_smoke -- --ignored --test-threads=1 -cargo test --test seed_only -- --ignored --test-threads=1 - -# Third-party-server tests (one container each): -cargo test --test integration_radicale -- --ignored --test-threads=1 -cargo test --test integration_baikal -- --ignored --test-threads=1 -cargo test --test integration_webdav -- --ignored --test-threads=1 -cargo test --test integration_dovecot -- --ignored --test-threads=1 -cargo test --test integration_cyrus -- --ignored --test-threads=1 - -# Slow tests -cargo test --test mock_jmap -- --ignored -``` - -`--test-threads=1` is mandatory, not just advisory: within a binary every test shares a single per-binary container, and each test provisions then tears down the same disposable `vandelay.org` domain (and opens the archive with SQLite `EXCLUSIVE` locking). Separate binaries are isolated (each boots its own container on dynamic host ports), so plain `cargo test --test ` invocations are safe to run one after another. +The tests in a binary share one container and one disposable domain, so a +second thread would trip over the first. [docs/usage.md](docs/usage.md) lists +every live test binary. ## License -Licensed under either of +Apache-2.0 OR MIT, at your option. The texts are in [LICENSES](LICENSES). - * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +Copyright (C) 2020, Stalwart Labs LLC
+Copyright (C) 2026, John Coffey -at your option. - -## Copyright - -Copyright (C) 2020, Stalwart Labs LLC +Forked from Vandelay, originally developed by Stalwart Labs, and distributed +under the same Apache-2.0 OR MIT terms. From f03e197d0bd64430fae2d79bf7d20ed7ae59b017 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 10:22:35 -0700 Subject: [PATCH 5/6] docs: write dates the American way "October 1, 2026" and "April 1, 2027", matching the rest of the suite. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 673a425..887376c 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ advertises the types being written. The full command reference is in export, or in any directory tree laid out that way. - **Exchange Server** -- an on-premises mailbox, through EWS. - **Exchange Online** -- through Microsoft Graph. EWS is being retired in - Exchange Online: blocked from 1 October 2026 unless an administrator allows - the client, and switched off on 1 April 2027. Public folders are only + Exchange Online: blocked from October 1, 2026 unless an administrator allows + the client, and switched off on April 1, 2027. Public folders are only reachable through EWS. ## Credentials From 210c04997c1b87d453696f7fbb4b183882612a01 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 30 Sep 2026 10:23:30 -0700 Subject: [PATCH 6/6] Track the Gitea workflows and docs/usage.md The .gitignore inherited from upstream ignores every dotfile except .github, and every Markdown file except README and CHANGELOG. The Gitea workflows and docs/usage.md, which the README links to, were therefore never committed. Both are now excepted and added. --- .gitea/workflows/announce.yml | 20 +++ .gitea/workflows/ci.yml | 99 +++++++++++++ .gitignore | 2 + docs/usage.md | 253 ++++++++++++++++++++++++++++++++++ 4 files changed, 374 insertions(+) create mode 100644 .gitea/workflows/announce.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 docs/usage.md diff --git a/.gitea/workflows/announce.yml b/.gitea/workflows/announce.yml new file mode 100644 index 0000000..14dabb4 --- /dev/null +++ b/.gitea/workflows/announce.yml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 John Coffey +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# Announce each published release on the community forum, in this project's +# Announcements category (coffey-labs/actions discourse-release; the repo -> +# category map is its release-map.json). Safe to re-run: one topic per tag. +name: announce + +on: + release: + types: [published] + +jobs: + announce: + runs-on: light + steps: + - uses: coffey-labs/actions/discourse-release@baedbb0e89336e49dd5105a22f8ea4f712b453ad + with: + api-key: ${{ secrets.DISCOURSE_RELEASE_KEY }} + discord-webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..1b8ca9b --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: 2026 John Coffey +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# CI on the self-hosted Gitea. Gitea reads .gitea/workflows and ignores +# .github/ once this directory exists; .github/workflows is GitHub's side of +# the switch below. +# +# Every job runs in an image pinned by digest (tag in the trailing comment), +# and the only actions used are coffey-labs/actions ones pinned by SHA. The +# instance resolves short `uses:` against itself, never GitHub, so nothing +# unreviewed can be pulled in. +# +# BUILD ON GITHUB. The org variable BUILD_ON decides which forge builds. +# Set to 'github', the test job below skips and .github/workflows/ci.yml does +# the work on GitHub's runners -- GitHub holds a push mirror of this +# repository, updated on every commit -- and reports back as the commit status +# "github/ci (branch)" or "github/ci (tag)". The `github` job waits for that +# status and passes or fails with it, so a run here still says whether the +# commit is good. Unset (or anything but 'github'), the tests run here. +# +# Releases are built only on GitHub: each architecture is compiled on a +# native runner there, and these runners are amd64 only. With BUILD_ON unset +# a v* tag is tested here but not released. +name: ci + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # The unit and mock tests; the #[ignore]d ones need live servers or + # containers and run by hand (README, "Testing"). + test: + if: ${{ vars.BUILD_ON != 'github' }} + runs-on: light + container: + image: rust:1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e # 1-bookworm + steps: + - uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec + - run: cargo test --locked + + # Stands in for test and release while GitHub builds: waits for the status + # GitHub's ci.yml posts on this commit and passes or fails with it. A pull + # request is checked at its head commit, which is what GitHub built when + # the branch reached the mirror. Two and a half hours covers a slow queue; + # a timeout here with BUILD_ON=github usually means GitHub never got the + # push -- check the mirror's last error in the repository settings. + github: + if: ${{ vars.BUILD_ON == 'github' }} + # Its own runner label with plenty of slots: this job only polls, but holds a slot + # for as long as the GitHub build takes, and must not starve the build runners. + runs-on: wait + timeout-minutes: 150 + container: + image: rust:1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e # 1-bookworm + steps: + - shell: bash + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + set -euo pipefail + apt-get update -qq && apt-get install -y -qq --no-install-recommends jq >/dev/null + ctx="github/ci (branch)"; [ "$IS_TAG" = true ] && ctx="github/ci (tag)" + echo "waiting for '$ctx' on $SHA" + while :; do + s="$(curl -fsS -H "Authorization: token $TOKEN" \ + "$CI_SERVER_INTERNAL/api/v1/repos/$REPO/commits/$SHA/statuses?limit=50" \ + | jq -c --arg c "$ctx" '[.[] | select(.context == $c)] | sort_by(.id) | last // empty')" || s="" + state="$(printf '%s' "$s" | jq -r '.status // .state // empty')" + case "$state" in + success) echo "GitHub: success"; exit 0 ;; + failure|error) echo "GitHub: $state -- $(jq -r '.target_url' <<<"$s")"; exit 1 ;; + esac + sleep 20 + done + + # GitHub makes the Release and publishes it only once its files are + # attached; this waits for GitHub's success and announces then. The action + # makes one topic per tag, so announce.yml firing for the same Release is a + # no-op. + announce: + needs: [github] + if: ${{ startsWith(github.ref, 'refs/tags/v') && needs.github.result == 'success' }} + runs-on: light + steps: + - uses: coffey-labs/actions/discourse-release@baedbb0e89336e49dd5105a22f8ea4f712b453ad + with: + api-key: ${{ secrets.DISCOURSE_RELEASE_KEY }} + discord-webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} + tag: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index b52a849..d2d71e3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ target !CHANGELOG.md .* !.github +!.gitea +!/docs/*.md diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..b6982c3 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,253 @@ + + +# Using inbuxa-migrate + +The full command reference. The [README](../README.md) says what the tool is +and how to install it. + +## Commands and global flags + +``` +inbuxa-migrate [args...] +``` + +Every command takes a few global flags -- verbosity, worker pool size, retry +policy, TLS handling -- besides its own. The ones that matter most: + +| Flag | Purpose | +| --- | --- | +| `-j, --threads ` | Worker pool size (default: logical CPUs). | +| `--dry-run` | Compute the full plan; perform no writes. | +| `-v`, `-vv`, `-vvv` | Increase log verbosity. | +| `-q, --quiet` | Warnings and errors only. | +| `--max-retries ` | Max retries per request on transient failures (default 5). | +| `--allow-invalid-certs` | Accept self-signed / invalid TLS certs. | + +Secrets come from the `INBUXA_MIGRATE_*` environment variables or a prompt; +see [Credentials](../README.md#credentials). The command line takes them too, +but should not be given them. + +## Import + +``` +inbuxa-migrate import [source-args...] +``` + +Reads a source account into the SQLite `ARCHIVE`, creating it if it is absent. +An archive remembers which account filled it; every importer takes +`--allow-source-change` to fill it from a different one anyway. + +### JMAP + +``` +inbuxa-migrate import jmap \ + --url \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ + (--account-id | --account-name ) \ + [--objects ] \ + +``` + +Imports one JMAP account, from inbuxa, Stalwart or any other JMAP server. `--objects` accepts a comma-separated list of object tokens (`mailbox,email,calendar,calendarevent,addressbook,contactcard,identity,sievescript,participantidentity,filenode`). The default is everything the server advertises. + +### IMAP + +``` +inbuxa-migrate import imap \ + --url imap(s)://host[:port] \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN] --auth-user ) \ + [--include ...] [--exclude ...] [--exclude-special ...] \ + [--folder ...] [--subscribed-only] [--noautomap] \ + [--include-deleted] [--allow-cleartext] [--compress] \ + [--fetch-batch ] [--imap-connections <1..8>] \ + +``` + +Imports mail, and only mail, from any IMAP server. Folders are chosen with +`--include` and `--exclude` patterns, or by exact name with `--folder`, but +not both. `--exclude-special` drops folders by SPECIAL-USE role. + +### CalDAV + +``` +inbuxa-migrate import caldav \ + --url \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ + [--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch ] \ + +``` + +Discovers the user's CalDAV principal (or accepts a URL pointing straight at a calendar-home or calendar), then imports calendars and events. + +### CardDAV + +``` +inbuxa-migrate import carddav \ + --url \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ + [--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch ] \ + +``` + +Same shape as `caldav`, but for address books and contacts. + +### WebDAV + +``` +inbuxa-migrate import webdav \ + --url \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ + [--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch ] \ + +``` + +Imports a plain WebDAV file collection as a JMAP `FileNode` tree. + +### ManageSieve + +``` +inbuxa-migrate import managesieve \ + --url sieve(s)://host[:port] \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN] --auth-user ) \ + [--allow-cleartext] \ + +``` + +Imports Sieve scripts only. Each script is stored once by content, and the +active one is recorded. + +### Maildir + +``` +inbuxa-migrate import maildir \ + [--include ...] [--exclude ...] [--folder ...] \ + [--noautomap] [--include-deleted] +``` + +Reads a local Maildir++ tree: a directory with `cur/`, `new/` and `tmp/`. No +network. Folders are chosen as for IMAP. + +### Google Takeout + +``` +inbuxa-migrate import takeout [--noautomap] +``` + +Finds every `.mbox`, `.ics` and `.vcf` file under a directory and imports it. +It is shaped for Google Takeout but reads any such tree. `--noautomap` stops +it giving Gmail's system labels their mailbox roles. + +### Microsoft Exchange (EWS) + +``` +inbuxa-migrate import exchange-ews \ + [--url ] [--mailbox ] \ + [--mailbox-kind primary|archive|public-folders] \ + (--auth-basic [--auth-password ] \ + | --auth-bearer [TOKEN] [--ews-tenant --ews-client-id \ + (--ews-device-code | --ews-client-secret )]) \ + [--ews-connections <1..8>] [--ews-getitem-batch ] [--ews-attachment-batch ] \ + [--ews-no-syncfolderitems] \ + +``` + +Imports a mailbox from an on-premises Exchange Server through EWS. Without +`--url` it uses Autodiscover, and then needs `--mailbox`. It signs in with +Basic, with a bearer token acquired beforehand, with OAuth's interactive +device-code flow, or with app-only client credentials. + +For Exchange Online, use `exchange-graph` instead. Microsoft is retiring EWS in Exchange Online: from October 1, 2026 it is blocked unless a tenant administrator sets `EwsEnabled` to `True` and adds the client id to `EwsAllowedAppIDs`, and on April 1, 2027 it is switched off for every tenant. On-premises Exchange Server is not affected. + +### Microsoft Exchange (Graph) + +``` +inbuxa-migrate import exchange-graph \ + (--client-id [--tenant ] | --access-token [TOKEN]) \ + [--user ] \ + [--mailbox-kind primary|archive] \ + [--objects mail,calendar,contacts] \ + [--event-body-format text|html] \ + [--graph-connections <1..16>] [--top <1..1000>] \ + +``` + +Imports a mailbox from Exchange Online through Microsoft Graph. Without +`--access-token` it signs in with the interactive device-code flow. `public-folders` is rejected here: Graph does not expose public folders, so they can only be imported with `exchange-ews`, which for Exchange Online is subject to the retirement described above. + +## Export + +``` +inbuxa-migrate export \ + --url \ + (--auth-basic [--auth-password ] | --auth-bearer [TOKEN]) \ + (--account-id | --account-name ) \ + [--objects ] [--prune [--yes]] \ + +``` + +Writes `ARCHIVE` into an account on a JMAP server, usually inbuxa. It keeps no +state of its own: every run matches the archive against the target afresh. +By default it only adds and updates -- items that match are updated, the +rest are created, and anything already on the target that the archive does +not cover is left alone. + +`--prune` also deletes what is on the target and not in the archive. It asks +first; `--yes` answers for it, for scripts. Export speaks JMAP only. + +## Inspect + +``` +inbuxa-migrate inspect [TYPE] [--limit ] [--offset ] +``` + +Read-only dump of a local archive. This command never opens a network connection and never writes to the archive. + +- Omit `TYPE` for a per-type summary (counts of every object kind plus blob storage stats). +- Pass an object type to dump it: `mailbox`, `email`, `identity`, `sievescript`, `addressbook`, `contactcard`, `calendar`, `calendarevent`, `participantidentity`, `filenode`. +- `mailbox` and `filenode` render as a tree (`--limit`/`--offset` are ignored); all other types use a paginated list and respect `--limit` and `--offset`. + + +## Live tests + +The tests against real servers -- a Stalwart server as a JMAP source, and +Dovecot, Cyrus, Radicale, Baikal and Apache `mod_dav` -- are marked +`#[ignore]`. Each test binary boots its own throwaway container through +`testcontainers`, pulling the image on first use, so Docker must be running +(`docker info` succeeds) before they start. + +Run them one binary at a time, always with one test thread: + +```sh +cargo test --test sync_jmap -- --ignored --test-threads=1 # live JMAP import/export/convergence/prune +cargo test --test sync_imap -- --ignored --test-threads=1 +cargo test --test sync_managesieve -- --ignored --test-threads=1 +cargo test --test sync_maildir -- --ignored --test-threads=1 +cargo test --test sync_caldav -- --ignored --test-threads=1 +cargo test --test sync_carddav -- --ignored --test-threads=1 +cargo test --test sync_webdav -- --ignored --test-threads=1 +cargo test --test live_stalwart -- --ignored --test-threads=1 +cargo test --test seed_smoke -- --ignored --test-threads=1 +cargo test --test seed_only -- --ignored --test-threads=1 + +# Third-party-server tests (one container each): +cargo test --test integration_radicale -- --ignored --test-threads=1 +cargo test --test integration_baikal -- --ignored --test-threads=1 +cargo test --test integration_webdav -- --ignored --test-threads=1 +cargo test --test integration_dovecot -- --ignored --test-threads=1 +cargo test --test integration_cyrus -- --ignored --test-threads=1 + +# Slow tests +cargo test --test mock_jmap -- --ignored +``` + +One thread is required, not advised. The tests in a binary share its +container, and each one creates and removes the same disposable domain, +`inbuxa-migrate.org`, and opens its archive with SQLite's `EXCLUSIVE` lock. +Separate binaries each boot their own container on their own ports, so +running them one after another is safe. +