extern crate chrono; extern crate imap; extern crate lettre; extern crate memory_logger; use std::sync::{atomic::AtomicBool, Arc}; use tempfile::tempdir; use vomit_m2dir::{Flag, Flags, M2store}; use memory_logger::blocking::MemoryLogger; use vomit_m2sync::*; const USER: &str = "m2sync@localhost"; fn test_host() -> String { std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string()) } fn test_imap_port() -> u16 { std::env::var("TEST_IMAPS_PORT") .unwrap_or("3143".to_string()) .parse() .unwrap_or(3143) } fn clean_mailbox(session: &mut imap::Session) { session.select("INBOX").unwrap(); let inbox = session.search("ALL").unwrap(); if !inbox.is_empty() { session.uid_store("1:*", "+FLAGS (\\Deleted)").unwrap(); } session.expunge().unwrap(); } fn session(user: &str) -> imap::Session { let host = test_host(); let mut s = imap::ClientBuilder::new(&host, test_imap_port()) .mode(imap::ConnectionMode::Plaintext) // .danger_skip_tls_verify(true) .connect() .unwrap() .login(user, user) .unwrap(); s.debug = true; clean_mailbox(&mut s); s } fn gen_message(subject: &str, body: &str) -> lettre::Message { lettre::message::Message::builder() .from("sender@localhost".parse().unwrap()) .to("test@localhost".parse().unwrap()) .subject(subject) .body(body.to_string()) .unwrap() .into() } #[test] fn basic_sync() { // Can be run against // `docker run -it --rm -p 3025:25 -p 3110:110 -p 3143:143 -p 3465:465 -p 3993:993 outoforder/cyrus-imapd-tester:latest` let logger = MemoryLogger::setup(log::Level::Trace).unwrap(); let dir = tempdir().unwrap(); let m2store = M2store::create(dir.path()).unwrap(); assert_eq!(m2store.folders().count(), 0); let mbox = "INBOX"; let mut c = session(USER); c.run_command_and_read_response("ENABLE QRESYNC").unwrap(); // make a message to append let e = gen_message("My first e-mail", "Hello world"); // append message c.append(mbox, &e.formatted()).finish().unwrap(); let opts = SyncOptions { uid: String::from("default"), // The local m2dir to sync to local: String::from(m2store.root().to_str().unwrap()), // The IMAPS URL to sync from remote: String::from(format!("{}:{}", test_host(), test_imap_port())), // The user for IMAP authentication user: String::from(USER), // The password for IMAP authentication password: String::from(USER), // The number of threads to use threads: 1, // Disable TLS certificate checks (e.g. for self-signed certs) unsafe_tls: true, disable_tls: false, // Actually perform the actions list_mailbox_actions: false, // A list of wildcard patterns to include only folders that match any of them. include: Vec::new(), // A list of wildcard patterns to exclude all folders that match any of them. exclude: Vec::new(), // Confirm execution of potentially dangerous actions (e.g. deleting mailboxes) force: true, }; let abort = Arc::new(AtomicBool::new(false)); // Initial pull to create local INBOX pull(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); assert!(logs.contains("Creating local mailbox INBOX")); drop(logs); logger.clear(); // Do another pull to verify that nothing happens pull(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); // assert!(logs.contains("Nothing to do for INBOX")); assert!(logs.contains("Skipping INBOX because HIGHESTMODSEQ is in sync")); drop(logs); logger.clear(); assert_eq!(m2store.folders().count(), 1); let m2dir = m2store.folders().into_iter().next().unwrap().unwrap(); assert_eq!(m2dir.name(), "INBOX"); // Compare local to remote let inbox = c.uid_search("ALL").unwrap(); assert_eq!(inbox.len(), 1); // delete email remotely clean_mailbox(&mut c); // the e-mail should be gone now let inbox = c.search("ALL").unwrap(); assert_eq!(inbox.len(), 0); // A push must bring it back push(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); assert!(logs.contains("restoring mail deleted on remote")); assert!(logs.contains("uploading local mail")); drop(logs); logger.clear(); // Another push should do nothing push(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); assert!(logs.contains("Skipping INBOX because HIGHESTMODSEQ is in sync")); drop(logs); logger.clear(); // Assert we didn't delete anything assert_eq!(m2dir.as_ref().count(), 1); // Assert it's back in mailbox let inbox = c.search("ALL").unwrap(); assert_eq!(inbox.len(), 1); // Store a new mail locally let e = gen_message("My second e-mail", "Hello again, world"); let msg = m2dir .as_ref() .store(&e.formatted(), &Flags::from([Flag::Seen])) .unwrap(); // and add another one remotely let e = gen_message("My third e-mail", "Good bye, world"); c.append(mbox, &e.formatted()).finish().unwrap(); // sync both ways sync(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); drop(logs); logger.clear(); // another sync should do nothing sync(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); assert!(logs.contains("Skipping INBOX because HIGHESTMODSEQ is in sync")); drop(logs); logger.clear(); // There should be three everywhere now assert_eq!(m2dir.as_ref().count(), 3); let inbox = c.uid_search("ALL").unwrap(); assert_eq!(inbox.len(), 3); // Delete a mail locally msg.delete().unwrap(); // another sync should delete it remotely sync(&opts, abort.clone()).unwrap(); let logs = logger.read(); let text: &str = &logs; println!("{}", text); assert!(logs.contains("deleting remote UID")); drop(logs); logger.clear(); // There should be two everywhere now assert_eq!(m2dir.as_ref().count(), 2); let inbox = c.uid_search("ALL").unwrap(); assert_eq!(inbox.len(), 2); }