diff --git a/Cargo.lock b/Cargo.lock index e74e8d5..1064f1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayref" @@ -103,9 +103,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "assert-json-diff" @@ -119,15 +119,15 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" +checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd" dependencies = [ - "filetime", "futures-core", "libc", "portable-atomic", "rustc-hash", + "rustix 0.38.44", "tokio", "tokio-stream", "xattr", @@ -180,9 +180,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -190,14 +190,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -384,9 +385,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -408,9 +409,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures", @@ -586,18 +587,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "darling" @@ -742,20 +743,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.10.1", + "rand 0.10.2", "web-time", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -979,9 +970,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" dependencies = [ "hashbrown 0.17.1", ] @@ -1422,19 +1413,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -1458,6 +1449,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1586,9 +1583,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -1854,9 +1851,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.40.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -1894,9 +1891,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2027,9 +2024,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2040,6 +2037,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2049,7 +2059,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -2083,9 +2093,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "zeroize", ] @@ -2152,9 +2162,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -2505,7 +2515,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2582,9 +2592,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", "num-conv", @@ -2602,9 +2612,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -2883,9 +2893,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "sha1_smol", ] @@ -2969,9 +2979,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2982,9 +2992,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2992,9 +3002,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3005,9 +3015,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -3148,6 +3158,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3297,7 +3316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -3325,18 +3344,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f554878..5585e70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs rustls-platform-verifier = "0.7" regex = "1.12" flate2 = "1.1.9" -quick-xml = "0.40" +quick-xml = "0.41" chrono = { version = "0.4", default-features = false, features = ["std"] } chrono-tz = { version = "0.10", default-features = false } diff --git a/src/sync/export/tree.rs b/src/sync/export/tree.rs index ffac84d..b219dfa 100644 --- a/src/sync/export/tree.rs +++ b/src/sync/export/tree.rs @@ -93,6 +93,44 @@ fn depth(local: i64, by: &HashMap>) -> usize { d } +fn already_exists_id(err: &Value) -> Option { + if err.get("type").and_then(Value::as_str) == Some("alreadyExists") { + err.get("existingId") + .and_then(Value::as_str) + .map(str::to_owned) + } else { + None + } +} + +fn find_name_collision( + targets: &[TargetNode], + parent_target: Option<&str>, + name: &str, +) -> Option { + targets + .iter() + .find(|t| t.parent.as_deref() == parent_target && fold_name(&t.name) == fold_name(name)) + .map(|t| t.id.clone()) +} + +fn record_merge( + ty: ObjectType, + local: i64, + target_id: String, + reason: &str, + maps: &mut Maps, + tmatched: &mut HashSet, + logger: &Logger, +) { + logger.warn(&format!( + "{} local {local} merged into existing target {target_id}: {reason}", + ty.jmap_name() + )); + maps.insert(ty, local, JmapId(target_id.clone())); + tmatched.insert(target_id); +} + pub fn reconcile( ctx: &Context, net: &Net, @@ -170,16 +208,33 @@ pub fn reconcile( .collect(); if interleave { for n in &level { - if let Some(p) = n.parent - && maps.target(ty, p).is_none() - { - logger.warn(&format!( - "{} local {} skipped: parent {} not created", - ty.jmap_name(), + let parent_target = match n.parent { + None => None, + Some(p) => match maps.target(ty, p) { + Some(t) => Some(t.0), + None => { + logger.warn(&format!( + "{} local {} skipped: parent {} not created", + ty.jmap_name(), + n.local, + p + )); + counts.failed += 1; + continue; + } + }, + }; + if let Some(tid) = find_name_collision(&targets, parent_target.as_deref(), &n.name) { + record_merge( + ty, n.local, - p - )); - counts.failed += 1; + tid, + "name already present on target", + maps, + &mut tmatched, + logger, + ); + counts.skipped += 1; continue; } let cid = format!("c{}", n.local); @@ -228,6 +283,20 @@ pub fn reconcile( } } for (cid, err) in &outcome.not_created { + let local = cid.strip_prefix('c').and_then(|s| s.parse::().ok()); + if let (Some(local), Some(existing)) = (local, already_exists_id(err)) { + record_merge( + ty, + local, + existing, + "alreadyExists: reusing existing target", + maps, + &mut tmatched, + logger, + ); + counts.skipped += 1; + continue; + } logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name())); counts.failed += 1; } @@ -236,16 +305,33 @@ pub fn reconcile( } let mut batch: Vec<(String, Value)> = Vec::new(); for n in &level { - if let Some(p) = n.parent - && maps.target(ty, p).is_none() - { - logger.warn(&format!( - "{} local {} skipped: parent {} not created", - ty.jmap_name(), + let parent_target = match n.parent { + None => None, + Some(p) => match maps.target(ty, p) { + Some(t) => Some(t.0), + None => { + logger.warn(&format!( + "{} local {} skipped: parent {} not created", + ty.jmap_name(), + n.local, + p + )); + counts.failed += 1; + continue; + } + }, + }; + if let Some(tid) = find_name_collision(&targets, parent_target.as_deref(), &n.name) { + record_merge( + ty, n.local, - p - )); - counts.failed += 1; + tid, + "name already present on target", + maps, + &mut tmatched, + logger, + ); + counts.skipped += 1; continue; } match build_create(ctx, ty, n.local, maps, &mut uploader) { @@ -290,6 +376,20 @@ pub fn reconcile( } } for (cid, err) in &outcome.not_created { + let local = cid.strip_prefix('c').and_then(|s| s.parse::().ok()); + if let (Some(local), Some(existing)) = (local, already_exists_id(err)) { + record_merge( + ty, + local, + existing, + "alreadyExists: reusing existing target", + maps, + &mut tmatched, + logger, + ); + counts.skipped += 1; + continue; + } logger.warn(&format!("{} {cid} not created: {err}", ty.jmap_name())); counts.failed += 1; } diff --git a/tests/mock_sync.rs b/tests/mock_sync.rs index 58ce1b0..bdb82e4 100644 --- a/tests/mock_sync.rs +++ b/tests/mock_sync.rs @@ -173,6 +173,319 @@ fn export_email_already_exists_is_matched_not_failed() { let _ = std::fs::remove_file(&archive); } +#[test] +fn export_mailbox_name_collision_merges_without_create() { + let mut server = mockito::Server::new(); + let base = server.url(); + let api = "/jmap/api"; + + let archive = tmp(); + { + let conn = db::init::open(&archive).unwrap(); + conn.execute( + "INSERT INTO mailboxes (id,name,parent_id,role) VALUES + (1,'Junk Email',NULL,'junk'), + (2,'Junk Mail',NULL,NULL)", + [], + ) + .unwrap(); + let blob = db::blobs::intern_blob( + &conn, + b"From: a@x\r\nSubject: spam\r\nMessage-ID: \r\n\r\nbody", + ) + .unwrap(); + conn.execute( + "INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords) + VALUES (?1,'2020-01-01T00:00:00Z','[2]','[\"$seen\"]')", + rusqlite::params![blob], + ) + .unwrap(); + } + + let _root = server.mock("GET", "/").with_status(404).create(); + let _wk = server + .mock("GET", "/.well-known/jmap") + .with_body(session_body(&base)) + .create(); + + let _mq1 = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/query".into())) + .with_body( + json!({"methodResponses":[["Mailbox/query", + {"accountId":"w","ids":["c"]},"q"]]}) + .to_string(), + ) + .expect(1) + .create(); + let _mq2 = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/query".into())) + .with_body( + json!({"methodResponses":[["Mailbox/query", + {"accountId":"w","ids":[]},"q"]]}) + .to_string(), + ) + .expect(1) + .create(); + let _mg = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/get".into())) + .with_body( + json!({"methodResponses":[["Mailbox/get",{"accountId":"w","list":[ + {"id":"c","name":"Junk Mail","role":"junk","parentId":null, + "myRights":{"mayDelete":true}}],"notFound":[]},"g"]]}) + .to_string(), + ) + .expect(1) + .create(); + + let no_set = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/set".into())) + .expect(0) + .create(); + + let _eq = server + .mock("POST", api) + .match_body(Matcher::Regex("Email/query".into())) + .with_body( + json!({"methodResponses":[["Email/query", + {"accountId":"w","ids":[]},"q"]]}) + .to_string(), + ) + .expect(1) + .create(); + let _up = server + .mock("POST", Matcher::Regex("/jmap/upload/".into())) + .with_body(json!({"blobId":"UPLOADED"}).to_string()) + .expect(1) + .create(); + let import_into_c = server + .mock("POST", api) + .match_body(Matcher::AllOf(vec![ + Matcher::Regex("Email/import".into()), + Matcher::Regex(r#""c":true"#.into()), + ])) + .with_body( + json!({"methodResponses":[["Email/import",{"accountId":"w", + "created":{"e1":{"id":"E1","blobId":"b","threadId":"t","size":10}}},"i"]]}) + .to_string(), + ) + .expect(1) + .create(); + + let summary = sync::export::run( + CommonConfig { + archive: archive.clone(), + threads: 1, + dry_run: false, + max_retries: 1, + allow_invalid_certs: false, + logger: Logger::from_flags(true, 0), + }, + ExportConfig { + connect: ConnectConfig { + url: base.clone(), + auth: Auth::Basic { + user: "u".into(), + password: "p".into(), + }, + account: AccountSelector::Id("w".into()), + }, + objects: None, + prune: false, + yes: true, + }, + ) + .expect("export run"); + + let mailbox = summary + .per_type + .iter() + .find(|(t, _)| *t == "Mailbox") + .map(|(_, c)| c.clone()) + .expect("mailbox counts"); + assert_eq!(mailbox.created, 0, "no mailbox is created"); + assert_eq!(mailbox.failed, 0, "the name collision is not a failure"); + assert_eq!( + mailbox.skipped, 2, + "role-matched Junk Email + merged Junk Mail" + ); + + let email = summary + .per_type + .iter() + .find(|(t, _)| *t == "Email") + .map(|(_, c)| c.clone()) + .expect("email counts"); + assert_eq!(email.created, 1, "the Junk Mail email lands on the target"); + assert_eq!(email.failed, 0); + assert!(!summary.any_failed()); + + no_set.assert(); + import_into_c.assert(); + let _ = std::fs::remove_file(&archive); +} + +#[test] +fn export_mailbox_already_exists_maps_existing_id() { + let mut server = mockito::Server::new(); + let base = server.url(); + let api = "/jmap/api"; + + let archive = tmp(); + { + let conn = db::init::open(&archive).unwrap(); + conn.execute( + "INSERT INTO mailboxes (id,name,parent_id,role) VALUES (1,'Junk Mail',NULL,NULL)", + [], + ) + .unwrap(); + let blob = db::blobs::intern_blob( + &conn, + b"From: a@x\r\nSubject: spam\r\nMessage-ID: \r\n\r\nbody", + ) + .unwrap(); + conn.execute( + "INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords) + VALUES (?1,'2020-01-01T00:00:00Z','[1]','[\"$seen\"]')", + rusqlite::params![blob], + ) + .unwrap(); + } + + let _root = server.mock("GET", "/").with_status(404).create(); + let _wk = server + .mock("GET", "/.well-known/jmap") + .with_body(session_body(&base)) + .create(); + + let _mq1 = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/query".into())) + .with_body( + json!({"methodResponses":[["Mailbox/query", + {"accountId":"w","ids":["t1"]},"q"]]}) + .to_string(), + ) + .expect(1) + .create(); + let _mq2 = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/query".into())) + .with_body( + json!({"methodResponses":[["Mailbox/query", + {"accountId":"w","ids":[]},"q"]]}) + .to_string(), + ) + .expect(1) + .create(); + let _mg = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/get".into())) + .with_body( + json!({"methodResponses":[["Mailbox/get",{"accountId":"w","list":[ + {"id":"t1","name":"Inbox","role":"inbox","parentId":null, + "myRights":{"mayDelete":true}}],"notFound":[]},"g"]]}) + .to_string(), + ) + .expect(1) + .create(); + + let set_collides = server + .mock("POST", api) + .match_body(Matcher::Regex("Mailbox/set".into())) + .with_body( + json!({"methodResponses":[["Mailbox/set",{"accountId":"w", + "notCreated":{"c1":{"type":"alreadyExists","existingId":"c"}}},"s"]]}) + .to_string(), + ) + .expect(1) + .create(); + + let _eq = server + .mock("POST", api) + .match_body(Matcher::Regex("Email/query".into())) + .with_body( + json!({"methodResponses":[["Email/query", + {"accountId":"w","ids":[]},"q"]]}) + .to_string(), + ) + .expect(1) + .create(); + let _up = server + .mock("POST", Matcher::Regex("/jmap/upload/".into())) + .with_body(json!({"blobId":"UPLOADED"}).to_string()) + .expect(1) + .create(); + let import_into_c = server + .mock("POST", api) + .match_body(Matcher::AllOf(vec![ + Matcher::Regex("Email/import".into()), + Matcher::Regex(r#""c":true"#.into()), + ])) + .with_body( + json!({"methodResponses":[["Email/import",{"accountId":"w", + "created":{"e1":{"id":"E1","blobId":"b","threadId":"t","size":10}}},"i"]]}) + .to_string(), + ) + .expect(1) + .create(); + + let summary = sync::export::run( + CommonConfig { + archive: archive.clone(), + threads: 1, + dry_run: false, + max_retries: 1, + allow_invalid_certs: false, + logger: Logger::from_flags(true, 0), + }, + ExportConfig { + connect: ConnectConfig { + url: base.clone(), + auth: Auth::Basic { + user: "u".into(), + password: "p".into(), + }, + account: AccountSelector::Id("w".into()), + }, + objects: None, + prune: false, + yes: true, + }, + ) + .expect("export run"); + + let mailbox = summary + .per_type + .iter() + .find(|(t, _)| *t == "Mailbox") + .map(|(_, c)| c.clone()) + .expect("mailbox counts"); + assert_eq!(mailbox.created, 0, "the create collided"); + assert_eq!( + mailbox.failed, 0, + "alreadyExists on Mailbox/set is not a failure" + ); + assert_eq!(mailbox.skipped, 1, "existingId folds into matched"); + + let email = summary + .per_type + .iter() + .find(|(t, _)| *t == "Email") + .map(|(_, c)| c.clone()) + .expect("email counts"); + assert_eq!(email.created, 1, "the email lands in the existing folder"); + assert_eq!(email.failed, 0); + assert!(!summary.any_failed()); + + set_collides.assert(); + import_into_c.assert(); + let _ = std::fs::remove_file(&archive); +} + #[test] fn email_export_sends_one_email_per_import_call() { let mut server = mockito::Server::new();