use std::{
fs,
path::Path,
process::Command,
time::{SystemTime, UNIX_EPOCH},
};
use io_pimdir::{
change::PimdirWriteOp,
client::PimdirStore,
collection::{PimdirCheckpoint, PimdirCollectionId},
};
const A: &str = "imap://127.0.0.1:143/INBOX";
const B: &str = "imap://127.0.0.1:144/INBOX";
const CRED: &str = "test@pimalaya.org:P!malaya-test-2026";
const ACCOUNT: &str = "dup";
fn marker() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("a clock after 1970")
.as_nanos();
format!("DUPMARKER{nanos}")
}
fn message(marker: &str) -> Vec<u8> {
format!(
"Message-ID: <{marker}@pimalaya.org>\r\n\
From: alice@pimalaya.org\r\n\
To: bob@pimalaya.org\r\n\
Subject: neverest duplicate identity\r\n\
Date: Tue, 25 Aug 2026 10:00:00 +0000\r\n\
\r\n\
{marker}\r\n",
)
.into_bytes()
}
#[test]
#[ignore = "requires two Stalwart instances (./tests/stalwart2.sh) on :143/:144 and --ignored"]
fn a_duplicated_identity_syncs_both_copies_and_settles() {
let tmp = tempfile::tempdir().expect("temp dir");
let root = tmp.path();
let state = root.join("state");
let config = root.join("config.toml");
let eml = root.join("msg.eml");
let marker = marker();
fs::create_dir_all(&state).unwrap();
fs::write(&eml, message(&marker)).unwrap();
append(&eml, A);
append(&eml, B);
append(&eml, B);
assert_eq!(uids(B, &marker).len(), 2, "B holds the message twice");
fs::write(&config, config_toml()).unwrap();
neverest(&["init", "-a", ACCOUNT], &config, &state);
let report = sync(&config, &state);
assert!(
report.get("ambiguous").is_none(),
"nothing is ambiguous any more: {report}"
);
assert!(
report.get("refused").is_none(),
"an IMAP server holds no UID to refuse: {report}"
);
assert_eq!(
uids(A, &marker).len(),
2,
"the second copy was appended to A: {report}"
);
let report = sync(&config, &state);
assert_eq!(
report["item"][""].as_array().map_or(0, Vec::len),
0,
"a settled collection reports nothing: {report}"
);
let mut expected = uids(B, &marker);
expected.sort();
expunge(B, &expected[0]);
assert_eq!(uids(B, &marker).len(), 1, "B now holds the message once");
let report = sync(&config, &state);
assert_eq!(
uids(A, &marker).len(),
1,
"exactly the deleted copy went: {report}"
);
drop_checkpoint(&state, "right");
let report = sync(&config, &state);
assert_eq!(
uids(A, &marker).len(),
1,
"the full enumeration re-appended nothing to A: {report}"
);
assert_eq!(uids(B, &marker).len(), 1, "nor to B: {report}");
}
fn config_toml() -> String {
format!(
"[accounts.{ACCOUNT}]\n\
sources.left.imap.server = \"{A}\"\n\
sources.left.imap.starttls = false\n\
sources.left.imap.sasl.plain.username = \"test@pimalaya.org\"\n\
sources.left.imap.sasl.plain.password.raw = \"P!malaya-test-2026\"\n\
targets.right.imap.server = \"{B}\"\n\
targets.right.imap.starttls = false\n\
targets.right.imap.sasl.plain.username = \"test@pimalaya.org\"\n\
targets.right.imap.sasl.plain.password.raw = \"P!malaya-test-2026\"\n",
)
}
fn drop_checkpoint(state: &Path, source: &str) {
let dir = state.join("neverest").join(ACCOUNT);
let mut store = PimdirStore::open(&dir)
.expect("open the account store")
.for_account(ACCOUNT)
.for_source(source);
store
.write(vec![PimdirWriteOp::SetCheckpoint {
collection: PimdirCollectionId("left/INBOX".into()),
checkpoint: PimdirCheckpoint(Vec::new()),
}])
.expect("drop the checkpoint");
}
fn append(eml: &Path, url: &str) {
let output = Command::new("curl")
.args(["-fsS", "-T"])
.arg(eml)
.args([url, "--user", CRED])
.output()
.expect("spawn curl append");
assert!(
output.status.success(),
"curl APPEND to {url} failed: {}",
String::from_utf8_lossy(&output.stderr),
);
}
fn uids(url: &str, marker: &str) -> Vec<String> {
let output = Command::new("curl")
.args([
"-fsS",
"--url",
url,
"--user",
CRED,
"-X",
&format!("UID SEARCH TEXT {marker}"),
])
.output()
.expect("spawn curl search");
assert!(
output.status.success(),
"curl SEARCH on {url} failed: {}",
String::from_utf8_lossy(&output.stderr),
);
String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.filter(|token| token.parse::<u32>().is_ok())
.map(str::to_string)
.collect()
}
fn expunge(url: &str, uid: &str) {
for request in [
format!("UID STORE {uid} +Flags \\Deleted"),
format!("UID EXPUNGE {uid}"),
] {
let output = Command::new("curl")
.args(["-fsS", "--url", url, "--user", CRED, "-X", &request])
.output()
.expect("spawn curl store/expunge");
assert!(
output.status.success(),
"curl {request} on {url} failed: {}",
String::from_utf8_lossy(&output.stderr),
);
}
}
fn sync(config: &Path, state: &Path) -> serde_json::Value {
let stdout = neverest(&["--json", "sync", "-a", ACCOUNT], config, state);
serde_json::from_str(&stdout).expect("the report is JSON")
}
fn neverest(args: &[&str], config: &Path, state: &Path) -> String {
let output = Command::new(env!("CARGO_BIN_EXE_neverest"))
.args(["-c", &config.to_string_lossy()])
.args(args)
.env("XDG_STATE_HOME", state)
.output()
.expect("spawn neverest");
assert!(
output.status.success(),
"`neverest {}` failed:\n--- stdout ---\n{}\n--- stderr ---\n{}",
args.join(" "),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
String::from_utf8_lossy(&output.stdout).into_owned()
}