/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; 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", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() )); let _ = std::fs::remove_file(&p); p } fn common(archive: &Path) -> CommonConfig { CommonConfig { archive: archive.to_path_buf(), threads: 1, dry_run: false, max_retries: 1, allow_invalid_certs: false, logger: Logger::from_flags(true, 0), } } fn base_cfg(root: &Path) -> MaildirImportConfig { MaildirImportConfig { maildir: root.to_path_buf(), include: Vec::new(), exclude: Vec::new(), folder: Vec::new(), automap: true, include_deleted: false, allow_source_change: false, } } fn count(conn: &Connection, table: &str) -> i64 { conn.query_row(&format!("SELECT count(*) FROM {table}"), [], |r| r.get(0)) .unwrap() } fn ensure_maildir(root: &Path) { for sub in ["cur", "new", "tmp"] { fs::create_dir_all(root.join(sub)).unwrap(); } } fn ensure_subfolder(root: &Path, dotted: &str) -> PathBuf { let sub = root.join(dotted); for s in ["cur", "new", "tmp"] { fs::create_dir_all(sub.join(s)).unwrap(); } sub } fn write_message(folder: &Path, sub: &str, filename: &str, body: &[u8]) -> PathBuf { let dir = folder.join(sub); fs::create_dir_all(&dir).unwrap(); let p = dir.join(filename); fs::write(&p, body).unwrap(); p } fn rfc5322(id: &str, subject: &str, body: &str) -> Vec { format!( "From: sender@example.com\r\nTo: rcpt@example.com\r\n\ Subject: {subject}\r\nMessage-ID: <{id}@example.com>\r\n\ Date: Mon, 12 May 2025 10:00:00 +0000\r\n\r\n{body}\r\n" ) .into_bytes() } fn folder_mailbox_id(conn: &Connection, canonical: &str) -> Option { conn.query_row( "SELECT local_id FROM sync_id_maildir WHERE type_name = 'mailbox' AND folder = ?1 AND unique_id = ''", rusqlite::params![canonical], |r| r.get(0), ) .ok() } fn emails_for_folder(conn: &Connection, canonical: &str) -> Vec<(String, String)> { let mailbox_id = folder_mailbox_id(conn, canonical).expect("mailbox row"); let mut stmt = conn .prepare( "SELECT b.hash, e.keywords FROM emails e JOIN blobs b ON b.id = e.blob_id WHERE EXISTS ( SELECT 1 FROM json_each(e.mailbox_ids) WHERE value = ?1 ) ORDER BY e.id", ) .unwrap(); let rows = stmt .query_map(rusqlite::params![mailbox_id], |r| { let hash: Vec = r.get(0)?; let kw: String = r.get(1)?; Ok((hex(&hash), kw)) }) .unwrap(); rows.filter_map(|r| r.ok()).collect() } fn hex(bytes: &[u8]) -> String { let mut s = String::with_capacity(bytes.len() * 2); for b in bytes { use std::fmt::Write; let _ = write!(s, "{b:02x}"); } s } fn dovecot_fixture(root: &Path) -> HashMap<&'static str, Vec<&'static str>> { ensure_maildir(root); write_message( root, "cur", "1739471123.M001P00123V0000I0000aaaa.host01,S=128,W=131:2,S", &rfc5322("dovecot-inbox-1", "Welcome", "Welcome to Dovecot"), ); write_message( root, "cur", "1739471124.M002P00123V0000I0000aaab.host01,S=140,W=143:2,RS", &rfc5322("dovecot-inbox-2", "Re: Welcome", "Reply body"), ); write_message( root, "new", "1739471200.M003P00123V0000I0000aaac.host01,S=64,W=66", &rfc5322("dovecot-inbox-3", "Unread mail", "Body of the unread"), ); let sent = ensure_subfolder(root, ".Sent"); write_message( &sent, "cur", "1739472000.M001P00123V0000I0000bbb1.host01,S=80,W=82:2,S", &rfc5322("dovecot-sent-1", "Sent to friend", "Hi friend"), ); let drafts = ensure_subfolder(root, ".Drafts"); write_message( &drafts, "cur", "1739472100.M002P00123V0000I0000bbb2.host01,S=50,W=52:2,DS", &rfc5322("dovecot-draft-1", "Half-written", "Almost there"), ); let trash = ensure_subfolder(root, ".Trash"); write_message( &trash, "cur", "1739472200.M003P00123V0000I0000bbb3.host01,S=30,W=32:2,ST", &rfc5322("dovecot-trash-1", "Junk", "trashed"), ); ensure_subfolder(root, ".Archive"); let arch25 = ensure_subfolder(root, ".Archive.2025"); write_message( &arch25, "cur", "1739473000.M001P00123V0000I0000cccc.host01,S=200,W=204:2,S", &rfc5322("dovecot-arch25-1", "Last year", "Old mail"), ); write_message( &arch25, "cur", "1739473001.M002P00123V0000I0000cccd.host01,S=200,W=204:2,SF", &rfc5322("dovecot-arch25-2", "Important", "Flagged"), ); fs::write(root.join("dovecot.index.log"), b"index").unwrap(); fs::write(root.join("dovecot-uidlist"), b"uidlist").unwrap(); fs::create_dir_all(root.join(".dovecot.imap")).unwrap(); fs::write(root.join("cur/.uidvalidity"), b"sidecar").unwrap(); let mut expected: HashMap<&'static str, Vec<&'static str>> = HashMap::new(); expected.insert("INBOX", vec![]); expected.insert("Sent", vec![]); expected.insert("Drafts", vec![]); expected.insert("Trash", vec![]); expected.insert("Archive", vec![]); expected.insert("Archive.2025", vec![]); expected } #[test] fn dovecot_layout_imports_every_folder_with_correct_roles() { let td = TempDir::new().unwrap(); let expected = dovecot_fixture(td.path()); let archive = tmp_archive("dovecot"); let summary = run(common(&archive), base_cfg(td.path())).expect("import"); assert!(!summary.any_failed(), "import failed: {summary:?}"); let conn = Connection::open(&archive).unwrap(); for name in expected.keys() { assert!( folder_mailbox_id(&conn, name).is_some(), "missing mailbox row for {name:?}" ); } let inbox = emails_for_folder(&conn, "INBOX"); assert_eq!(inbox.len(), 3, "3 INBOX messages expected"); let seen_count = inbox.iter().filter(|(_, kw)| kw.contains("$seen")).count(); assert_eq!(seen_count, 2, "two cur/ msgs are $seen"); let sent = emails_for_folder(&conn, "Sent"); assert_eq!(sent.len(), 1); let trash = emails_for_folder(&conn, "Trash"); assert!(trash.is_empty(), "Trashed message skipped by default"); let arch_2025 = emails_for_folder(&conn, "Archive.2025"); assert_eq!(arch_2025.len(), 2); let flagged = arch_2025.iter().any(|(_, kw)| kw.contains("$flagged")); assert!(flagged, "one Archive.2025 message has $flagged"); let archive_parent_id = folder_mailbox_id(&conn, "Archive").unwrap(); let arch_2025_parent: Option = conn .query_row( "SELECT parent_id FROM mailboxes WHERE name = '2025'", [], |r| r.get(0), ) .unwrap(); assert_eq!(arch_2025_parent, Some(archive_parent_id)); let _ = fs::remove_file(&archive); } #[test] fn dovecot_include_deleted_keeps_trash_message() { let td = TempDir::new().unwrap(); dovecot_fixture(td.path()); let archive = tmp_archive("dovecot-include-deleted"); let mut cfg = base_cfg(td.path()); cfg.include_deleted = true; let summary = run(common(&archive), cfg).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); let trash = emails_for_folder(&conn, "Trash"); assert_eq!(trash.len(), 1); assert!(trash[0].1.contains("$deleted")); let _ = fs::remove_file(&archive); } fn courier_fixture(root: &Path) { ensure_maildir(root); write_message( root, "cur", "1234567890.12345_0.mail.example:2,S", &rfc5322("courier-1", "Hi", "Hello from Courier"), ); write_message( root, "new", "1234567891.12345_1.mail.example", &rfc5322("courier-2", "Unread", "New body"), ); let sent = ensure_subfolder(root, ".Sent"); write_message( &sent, "cur", "1234567900.12350_0.mail.example:2,S", &rfc5322("courier-sent-1", "Sent reply", "Bye"), ); let lists = ensure_subfolder(root, ".Lists.maildir-dev"); write_message( &lists, "cur", "1234568000.12400_0.mail.example:2,RS", &rfc5322("courier-list-1", "[Maildir-dev] thread", "list body"), ); fs::write(root.join("courierimapsubscribed"), b"INBOX.Sent\n").unwrap(); } #[test] fn courier_layout_with_orphan_parent_creates_ephemeral_lists_row() { let td = TempDir::new().unwrap(); courier_fixture(td.path()); let archive = tmp_archive("courier"); let summary = run(common(&archive), base_cfg(td.path())).expect("import"); assert!(!summary.any_failed(), "{summary:?}"); let conn = Connection::open(&archive).unwrap(); assert!(folder_mailbox_id(&conn, "INBOX").is_some()); assert!(folder_mailbox_id(&conn, "Sent").is_some()); let lists_id = folder_mailbox_id(&conn, "Lists").expect("ephemeral Lists row created"); let leaf_id = folder_mailbox_id(&conn, "Lists.maildir-dev").expect("Lists.maildir-dev row"); let leaf_parent: Option = conn .query_row( "SELECT parent_id FROM mailboxes WHERE id = ?1", rusqlite::params![leaf_id], |r| r.get(0), ) .unwrap(); assert_eq!(leaf_parent, Some(lists_id)); let role: Option = conn .query_row( "SELECT role FROM mailboxes WHERE id = ?1", rusqlite::params![lists_id], |r| r.get(0), ) .unwrap(); assert_eq!(role, None); assert_eq!(emails_for_folder(&conn, "INBOX").len(), 2); assert_eq!(emails_for_folder(&conn, "Sent").len(), 1); assert_eq!(emails_for_folder(&conn, "Lists.maildir-dev").len(), 1); let _ = fs::remove_file(&archive); } fn qmail_fixture(root: &Path) { ensure_maildir(root); write_message( root, "new", "1700000000.12345.box01", &rfc5322("qmail-1", "qmail one", "body 1"), ); write_message( root, "new", "1700000001.12346.box01", &rfc5322("qmail-2", "qmail two", "body 2"), ); write_message( root, "cur", "1700000002.12347.box01:2,S", &rfc5322("qmail-3", "Seen", "body 3"), ); } #[test] fn qmail_root_only_maildir_imports_inbox_only() { let td = TempDir::new().unwrap(); qmail_fixture(td.path()); let archive = tmp_archive("qmail"); let summary = run(common(&archive), base_cfg(td.path())).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); let mboxes = count(&conn, "mailboxes"); assert_eq!(mboxes, 1, "INBOX only"); let inbox = emails_for_folder(&conn, "INBOX"); assert_eq!(inbox.len(), 3); let seen = inbox.iter().filter(|(_, kw)| kw.contains("$seen")).count(); assert_eq!(seen, 1, "only the cur/ message has $seen"); let _ = fs::remove_file(&archive); } fn cyrus_export_fixture(root: &Path) { ensure_maildir(root); write_message( root, "cur", "1701000000.X1.cyrus-export:2,S", &rfc5322("cyrus-1", "Cyrus mail 1", "exported"), ); write_message( root, "cur", "1701000001.X2.cyrus-export:2,", &rfc5322("cyrus-2", "Cyrus mail 2", "exported"), ); let sent = ensure_subfolder(root, ".Sent"); write_message( &sent, "cur", "1701000100.X3.cyrus-export:2,S", &rfc5322("cyrus-sent-1", "Sent from Cyrus", "exported sent"), ); let arch = ensure_subfolder(root, ".Archive"); write_message( &arch, "cur", "1701000200.X4.cyrus-export:2,S", &rfc5322("cyrus-arch-1", "Archived", "old"), ); } #[test] fn cyrus_style_export_imports_cleanly() { let td = TempDir::new().unwrap(); cyrus_export_fixture(td.path()); let archive = tmp_archive("cyrus"); let summary = run(common(&archive), base_cfg(td.path())).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); assert_eq!(emails_for_folder(&conn, "INBOX").len(), 2); assert_eq!(emails_for_folder(&conn, "Sent").len(), 1); assert_eq!(emails_for_folder(&conn, "Archive").len(), 1); let sent_role: Option = conn .query_row("SELECT role FROM mailboxes WHERE name = 'Sent'", [], |r| { r.get(0) }) .unwrap(); assert_eq!(sent_role.as_deref(), Some("sent")); let _ = fs::remove_file(&archive); } #[test] fn include_filter_keeps_only_matching_folders() { let td = TempDir::new().unwrap(); dovecot_fixture(td.path()); let archive = tmp_archive("filter-include"); let mut cfg = base_cfg(td.path()); cfg.include = vec![regex::Regex::new(r"^(INBOX|Sent)$").unwrap()]; let summary = run(common(&archive), cfg).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); assert!(folder_mailbox_id(&conn, "INBOX").is_some()); assert!(folder_mailbox_id(&conn, "Sent").is_some()); assert!(folder_mailbox_id(&conn, "Trash").is_none()); assert!(folder_mailbox_id(&conn, "Archive.2025").is_none()); let _ = fs::remove_file(&archive); } #[test] fn exclude_filter_drops_matching_folders() { let td = TempDir::new().unwrap(); dovecot_fixture(td.path()); let archive = tmp_archive("filter-exclude"); let mut cfg = base_cfg(td.path()); cfg.exclude = vec![regex::Regex::new(r"^Trash$").unwrap()]; let summary = run(common(&archive), cfg).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); assert!(folder_mailbox_id(&conn, "Trash").is_none()); assert!(folder_mailbox_id(&conn, "Sent").is_some()); let _ = fs::remove_file(&archive); } #[test] fn explicit_folder_overrides_include_exclude() { let td = TempDir::new().unwrap(); dovecot_fixture(td.path()); let archive = tmp_archive("filter-folder"); let mut cfg = base_cfg(td.path()); cfg.folder = vec!["Sent".to_owned()]; let summary = run(common(&archive), cfg).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); let mbox_count = count(&conn, "mailboxes"); assert_eq!(mbox_count, 1); assert!(folder_mailbox_id(&conn, "Sent").is_some()); let _ = fs::remove_file(&archive); } #[test] fn noautomap_drops_roles_except_inbox() { let td = TempDir::new().unwrap(); dovecot_fixture(td.path()); let archive = tmp_archive("noautomap"); let mut cfg = base_cfg(td.path()); cfg.automap = false; let summary = run(common(&archive), cfg).expect("import"); assert!(!summary.any_failed()); let conn = Connection::open(&archive).unwrap(); let sent_role: Option = conn .query_row("SELECT role FROM mailboxes WHERE name = 'Sent'", [], |r| { r.get(0) }) .unwrap(); assert!(sent_role.is_none(), "automap off -> Sent has no role"); let inbox_role: Option = conn .query_row("SELECT role FROM mailboxes WHERE name = 'INBOX'", [], |r| { r.get(0) }) .unwrap(); assert_eq!(inbox_role.as_deref(), Some("inbox")); let _ = fs::remove_file(&archive); } #[test] fn dry_run_reports_diff_without_writing_emails() { let td = TempDir::new().unwrap(); dovecot_fixture(td.path()); let archive = tmp_archive("dry-run"); let mut common_cfg = common(&archive); common_cfg.dry_run = true; let summary = run(common_cfg, base_cfg(td.path())).expect("dryrun"); let (mailbox_label, mailbox_counts) = &summary.per_type[0]; let (email_label, email_counts) = &summary.per_type[1]; assert_eq!(*mailbox_label, "mailbox"); assert_eq!(*email_label, "email"); assert!(mailbox_counts.created >= 6); assert!(email_counts.created >= 6); let conn = Connection::open(&archive).unwrap(); assert_eq!(count(&conn, "emails"), 0, "dry-run wrote no emails"); assert_eq!(count(&conn, "mailboxes"), 0, "dry-run wrote no mailboxes"); let _ = fs::remove_file(&archive); } #[test] fn rejects_path_without_cur_subdir() { let td = TempDir::new().unwrap(); 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(_))); let _ = fs::remove_file(&archive); } #[test] fn rejects_dovecot_layout_fs_tree() { let td = TempDir::new().unwrap(); ensure_maildir(td.path()); for s in ["cur", "new", "tmp"] { fs::create_dir_all(td.path().join("Sent").join(s)).unwrap(); } 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) => { assert!(msg.contains("Maildir++"), "msg was: {msg}"); } other => panic!("expected Usage error, got {other:?}"), } let _ = fs::remove_file(&archive); } #[test] fn rejects_nonexistent_path() { let archive = tmp_archive("nonexistent"); 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(_))); let _ = fs::remove_file(&archive); } #[test] fn source_change_protection_refuses_second_path() { let td_a = TempDir::new().unwrap(); dovecot_fixture(td_a.path()); let td_b = TempDir::new().unwrap(); qmail_fixture(td_b.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(_))); let _ = fs::remove_file(&archive); } #[test] fn allow_source_change_unlocks_second_path() { let td_a = TempDir::new().unwrap(); dovecot_fixture(td_a.path()); let td_b = TempDir::new().unwrap(); qmail_fixture(td_b.path()); let archive = tmp_archive("allow-source-change"); run(common(&archive), base_cfg(td_a.path())).expect("first import"); let mut second = base_cfg(td_b.path()); second.allow_source_change = true; run(common(&archive), second).expect("second import permitted"); let _ = fs::remove_file(&archive); } #[test] fn folder_and_include_are_mutually_exclusive() { let td = TempDir::new().unwrap(); ensure_maildir(td.path()); let archive = tmp_archive("mutex-folder-include"); let mut cfg = base_cfg(td.path()); cfg.folder = vec!["INBOX".to_owned()]; cfg.include = vec![regex::Regex::new("^Sent$").unwrap()]; let err = run(common(&archive), cfg).expect_err("mutex"); match err { vandelay::error::Error::Usage(msg) => { assert!( msg.contains("--folder") && msg.contains("--include"), "msg was: {msg}" ); } other => panic!("expected Usage, got {other:?}"), } let _ = fs::remove_file(&archive); } #[test] fn folder_and_exclude_are_mutually_exclusive() { let td = TempDir::new().unwrap(); ensure_maildir(td.path()); let archive = tmp_archive("mutex-folder-exclude"); let mut cfg = base_cfg(td.path()); 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(_))); let _ = fs::remove_file(&archive); } #[test] fn three_level_folder_delete_is_leaf_first() { let td = TempDir::new().unwrap(); ensure_maildir(td.path()); ensure_subfolder(td.path(), ".A"); ensure_subfolder(td.path(), ".A.B"); let leaf = ensure_subfolder(td.path(), ".A.B.C"); write_message( &leaf, "cur", "1.M0.host:2,S", &rfc5322("3lvl-1", "x", "body"), ); let archive = tmp_archive("three-level-delete"); run(common(&archive), base_cfg(td.path())).expect("first import"); fs::remove_dir_all(td.path().join(".A.B.C")).unwrap(); fs::remove_dir_all(td.path().join(".A.B")).unwrap(); let summary = run(common(&archive), base_cfg(td.path())).expect("second import"); assert!(!summary.any_failed(), "{summary:?}"); let (_, mbox) = &summary.per_type[0]; let (_, email) = &summary.per_type[1]; assert_eq!(mbox.deleted, 2, "B and B.C deleted leaf-first"); assert_eq!(email.deleted, 1); let conn = Connection::open(&archive).unwrap(); assert!(folder_mailbox_id(&conn, "A").is_some()); assert!(folder_mailbox_id(&conn, "A.B").is_none()); assert!(folder_mailbox_id(&conn, "A.B.C").is_none()); let _ = fs::remove_file(&archive); } #[test] fn same_unique_id_in_two_folders_produces_two_rows_one_blob() { let td = TempDir::new().unwrap(); ensure_maildir(td.path()); let sent = ensure_subfolder(td.path(), ".Sent"); let body = rfc5322("dup-1", "shared", "shared body"); write_message(td.path(), "cur", "1.M0.host:2,S", &body); write_message(&sent, "cur", "1.M0.host:2,S", &body); let archive = tmp_archive("cross-folder-dup"); let summary = run(common(&archive), base_cfg(td.path())).expect("import"); assert!(!summary.any_failed(), "{summary:?}"); let conn = Connection::open(&archive).unwrap(); assert_eq!(count(&conn, "blobs"), 1, "BLAKE3 dedup"); assert_eq!(count(&conn, "emails"), 2, "one row per folder"); let inbox = emails_for_folder(&conn, "INBOX"); let sent_rows = emails_for_folder(&conn, "Sent"); assert_eq!(inbox.len(), 1); assert_eq!(sent_rows.len(), 1); assert_eq!(inbox[0].0, sent_rows[0].0); let _ = fs::remove_file(&archive); } #[cfg(unix)] #[test] fn unreadable_file_is_counted_and_warned_not_aborted() { use std::os::unix::fs::PermissionsExt; let td = TempDir::new().unwrap(); ensure_maildir(td.path()); write_message( td.path(), "cur", "1.M0.host:2,S", &rfc5322("ok-1", "ok", "ok"), ); let bad = write_message( td.path(), "cur", "2.M0.host:2,S", &rfc5322("bad-1", "denied", "denied"), ); let mut perms = fs::metadata(&bad).unwrap().permissions(); perms.set_mode(0o000); fs::set_permissions(&bad, perms).unwrap(); let archive = tmp_archive("unreadable"); let summary = run(common(&archive), base_cfg(td.path())).expect("not aborted"); let mut perms = fs::metadata(&bad).unwrap().permissions(); perms.set_mode(0o600); fs::set_permissions(&bad, perms).unwrap(); let (_, email) = &summary.per_type[1]; assert!(summary.any_failed(), "exit-5 worthy"); assert_eq!(email.failed, 1, "the bad file is counted"); assert_eq!(email.created, 1, "the readable file imports"); let _ = fs::remove_file(&archive); } #[test] fn pointing_at_a_dot_subfolder_warns_but_imports() { let parent = TempDir::new().unwrap(); ensure_maildir(parent.path()); let sub = ensure_subfolder(parent.path(), ".Sent"); write_message(&sub, "cur", "1.M0.host:2,S", &rfc5322("sub-1", "x", "x")); let archive = tmp_archive("dot-subfolder-as-root"); let summary = run(common(&archive), base_cfg(&sub)).expect("import proceeds"); assert!(!summary.any_failed(), "{summary:?}"); let conn = Connection::open(&archive).unwrap(); assert!(folder_mailbox_id(&conn, "INBOX").is_some()); assert_eq!(emails_for_folder(&conn, "INBOX").len(), 1); let _ = fs::remove_file(&archive); } #[test] fn malformed_message_yields_zero_message_match_but_imports() { let td = TempDir::new().unwrap(); ensure_maildir(td.path()); write_message(td.path(), "cur", "1.M0.host:2,S", b""); write_message( td.path(), "cur", "2.M0.host:2,S", b"X-Garbage: yes\r\n\r\nno headers", ); let archive = tmp_archive("malformed"); let summary = run(common(&archive), base_cfg(td.path())).expect("import"); assert!(!summary.any_failed(), "{summary:?}"); let conn = Connection::open(&archive).unwrap(); let inbox = emails_for_folder(&conn, "INBOX"); assert_eq!(inbox.len(), 2); let _ = fs::remove_file(&archive); }