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.
290 lines
8.9 KiB
Rust
290 lines
8.9 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*/
|
|
|
|
mod integration;
|
|
mod seeder;
|
|
|
|
use integration::stalwart::shared as shared_stalwart;
|
|
use seeder::jmap::Jmap;
|
|
use seeder::seed::SeedStats;
|
|
use serde_json::{Value, json};
|
|
|
|
const CORE: &str = "urn:ietf:params:jmap:core";
|
|
const MAIL: &str = "urn:ietf:params:jmap:mail";
|
|
const SUBMISSION: &str = "urn:ietf:params:jmap:submission";
|
|
const SIEVE: &str = "urn:ietf:params:jmap:sieve";
|
|
const CONTACTS: &str = "urn:ietf:params:jmap:contacts";
|
|
const CALENDARS: &str = "urn:ietf:params:jmap:calendars";
|
|
const FILENODE: &str = "urn:ietf:params:jmap:filenode";
|
|
const PRINCIPALS: &str = "urn:ietf:params:jmap:principals";
|
|
|
|
const DEFAULT_MAILBOXES: usize = 5;
|
|
|
|
fn list_len(v: &Value) -> usize {
|
|
v.get("list")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.len())
|
|
.unwrap_or(usize::MAX)
|
|
}
|
|
|
|
fn ids_len(v: &Value) -> usize {
|
|
v.get("ids")
|
|
.and_then(Value::as_array)
|
|
.map(|a| a.len())
|
|
.unwrap_or(usize::MAX)
|
|
}
|
|
|
|
fn bare() -> SeedStats {
|
|
SeedStats {
|
|
mailboxes_created: 0,
|
|
emails: 0,
|
|
contacts: 0,
|
|
events: 0,
|
|
sieve_active: None,
|
|
identity: false,
|
|
file_nodes: 0,
|
|
address_books: 1,
|
|
calendars: 1,
|
|
}
|
|
}
|
|
|
|
fn verify_account(label: &str, user: &Jmap, account_id: &str, s: &SeedStats) {
|
|
let mailboxes = user
|
|
.call(
|
|
&[CORE, MAIL],
|
|
"Mailbox/get",
|
|
account_id,
|
|
json!({ "properties": ["id", "role"] }),
|
|
)
|
|
.expect("Mailbox/get");
|
|
assert_eq!(
|
|
list_len(&mailboxes),
|
|
DEFAULT_MAILBOXES + s.mailboxes_created,
|
|
"{label}: mailbox count"
|
|
);
|
|
|
|
let emails = user
|
|
.call(
|
|
&[CORE, MAIL],
|
|
"Email/query",
|
|
account_id,
|
|
json!({ "calculateTotal": true }),
|
|
)
|
|
.expect("Email/query");
|
|
let email_total = emails
|
|
.get("total")
|
|
.and_then(Value::as_u64)
|
|
.map(|n| n as usize)
|
|
.unwrap_or_else(|| ids_len(&emails));
|
|
assert_eq!(email_total, s.emails, "{label}: email count");
|
|
|
|
let scripts = user
|
|
.call(
|
|
&[CORE, SIEVE],
|
|
"SieveScript/get",
|
|
account_id,
|
|
json!({ "properties": ["id", "name", "isActive"] }),
|
|
)
|
|
.expect("SieveScript/get");
|
|
match s.sieve_active {
|
|
Some(active) => {
|
|
let list = scripts
|
|
.get("list")
|
|
.and_then(Value::as_array)
|
|
.expect("sieve list");
|
|
assert_eq!(list.len(), 2, "{label}: sieve script count");
|
|
let primary = list
|
|
.iter()
|
|
.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),
|
|
Some(active),
|
|
"{label}: primary sieve isActive"
|
|
);
|
|
}
|
|
None => assert_eq!(list_len(&scripts), 0, "{label}: expected no sieve scripts"),
|
|
}
|
|
|
|
let identities = user
|
|
.call(
|
|
&[CORE, SUBMISSION],
|
|
"Identity/get",
|
|
account_id,
|
|
json!({ "properties": ["id", "name"] }),
|
|
)
|
|
.expect("Identity/get");
|
|
let custom = identities
|
|
.get("list")
|
|
.and_then(Value::as_array)
|
|
.map(|a| {
|
|
a.iter()
|
|
.filter(|i| {
|
|
i.get("name").and_then(Value::as_str) == Some(seeder::CUSTOM_IDENTITY_NAME)
|
|
})
|
|
.count()
|
|
})
|
|
.unwrap_or(0);
|
|
assert_eq!(
|
|
custom,
|
|
usize::from(s.identity),
|
|
"{label}: custom identity presence"
|
|
);
|
|
|
|
let files = user
|
|
.call(
|
|
&[CORE, FILENODE],
|
|
"FileNode/query",
|
|
account_id,
|
|
json!({ "calculateTotal": true }),
|
|
)
|
|
.expect("FileNode/query");
|
|
let file_total = files
|
|
.get("total")
|
|
.and_then(Value::as_u64)
|
|
.map(|n| n as usize)
|
|
.unwrap_or_else(|| ids_len(&files));
|
|
assert_eq!(file_total, s.file_nodes, "{label}: file node count");
|
|
|
|
let books = user
|
|
.call(
|
|
&[CORE, CONTACTS],
|
|
"AddressBook/get",
|
|
account_id,
|
|
json!({ "properties": ["id"] }),
|
|
)
|
|
.expect("AddressBook/get");
|
|
assert_eq!(
|
|
list_len(&books),
|
|
s.address_books,
|
|
"{label}: address book count"
|
|
);
|
|
|
|
let calendars = user
|
|
.call(
|
|
&[CORE, CALENDARS],
|
|
"Calendar/get",
|
|
account_id,
|
|
json!({ "properties": ["id"] }),
|
|
)
|
|
.expect("Calendar/get");
|
|
assert_eq!(list_len(&calendars), s.calendars, "{label}: calendar count");
|
|
|
|
let cards = user
|
|
.call(
|
|
&[CORE, CONTACTS],
|
|
"ContactCard/get",
|
|
account_id,
|
|
json!({ "properties": ["id"] }),
|
|
)
|
|
.expect("ContactCard/get");
|
|
assert_eq!(list_len(&cards), s.contacts, "{label}: contact card count");
|
|
|
|
let cal_events = user
|
|
.call(
|
|
&[CORE, CALENDARS],
|
|
"CalendarEvent/query",
|
|
account_id,
|
|
json!({ "calculateTotal": true }),
|
|
)
|
|
.expect("CalendarEvent/query");
|
|
let event_total = cal_events
|
|
.get("total")
|
|
.and_then(Value::as_u64)
|
|
.map(|n| n as usize)
|
|
.unwrap_or_else(|| ids_len(&cal_events));
|
|
assert_eq!(event_total, s.events, "{label}: calendar event count");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires Docker"]
|
|
fn provision_seed_verify_and_teardown() {
|
|
let stalwart = shared_stalwart();
|
|
let fixture = seeder::provision(stalwart.base_url()).expect("provision");
|
|
|
|
assert_eq!(fixture.domain, seeder::DOMAIN);
|
|
assert!(!fixture.domain_id.is_empty());
|
|
assert_eq!(
|
|
fixture.accounts.len(),
|
|
seeder::SYNC_IN.len() + seeder::SYNC_OUT.len() + 1
|
|
);
|
|
|
|
for lp in seeder::SYNC_IN {
|
|
let a = fixture.account(lp).expect("sync-in account");
|
|
let stats = a.seeded.as_ref().expect("sync-in seeded stats");
|
|
assert!(stats.emails > 0, "{lp}: expected seeded emails");
|
|
let user = Jmap::connect(&fixture.base_url, &a.email, &a.password).expect("login sync-in");
|
|
assert_eq!(user.account_id, a.account_id);
|
|
verify_account(lp, &user, &a.account_id, stats);
|
|
}
|
|
|
|
let baseline = bare();
|
|
for lp in seeder::SYNC_OUT {
|
|
let a = fixture.account(lp).expect("sync-out account");
|
|
assert!(a.seeded.is_none(), "{lp}: targets must not be seeded");
|
|
let user = Jmap::connect(&fixture.base_url, &a.email, &a.password).expect("login sync-out");
|
|
verify_account(lp, &user, &a.account_id, &baseline);
|
|
}
|
|
|
|
let admin_fx = fixture
|
|
.account(seeder::ADMIN_LOCALPART)
|
|
.expect("admin account");
|
|
assert!(admin_fx.admin_role);
|
|
let admin = Jmap::connect(&fixture.base_url, &admin_fx.email, &admin_fx.password)
|
|
.expect("login admin account");
|
|
let test1 = fixture.account("test1").expect("test1");
|
|
let resolved = admin
|
|
.request(
|
|
&[CORE, PRINCIPALS],
|
|
json!([
|
|
["Principal/query", { "filter": { "name": test1.email } }, "q"],
|
|
["Principal/get", {
|
|
"#ids": { "resultOf": "q", "name": "Principal/query", "path": "/ids" },
|
|
"properties": ["id", "name", "accounts"]
|
|
}, "g"]
|
|
]),
|
|
)
|
|
.expect("Principal discovery");
|
|
let principals = resolved
|
|
.get("methodResponses")
|
|
.and_then(Value::as_array)
|
|
.and_then(|r| r.get(1))
|
|
.and_then(|r| r.get(1))
|
|
.and_then(|g| g.get("list"))
|
|
.and_then(Value::as_array)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
let exact: Vec<&Value> = principals
|
|
.iter()
|
|
.filter(|p| p.get("name").and_then(Value::as_str) == Some(test1.email.as_str()))
|
|
.collect();
|
|
assert_eq!(exact.len(), 1, "admin Principal exact-name resolution");
|
|
let owner = exact[0]
|
|
.get("accounts")
|
|
.and_then(Value::as_object)
|
|
.and_then(|m| {
|
|
m.iter().find_map(|(_, v)| {
|
|
v.get("urn:ietf:params:jmap:principals:owner")
|
|
.and_then(|o| o.get("accountIdForPrincipal"))
|
|
.and_then(Value::as_str)
|
|
})
|
|
})
|
|
.expect("accountIdForPrincipal");
|
|
assert_eq!(owner, test1.account_id, "resolved account id matches test1");
|
|
|
|
seeder::teardown(stalwart.base_url()).expect("teardown");
|
|
|
|
let (admin_user, admin_password) = &fixture.admin_login;
|
|
let gone = seeder::admin::Admin::connect(&fixture.base_url, admin_user, admin_password)
|
|
.expect("admin reconnect")
|
|
.domain_id(seeder::DOMAIN)
|
|
.expect("domain_id query");
|
|
assert!(gone.is_none(), "domain should be gone after teardown");
|
|
}
|