vomit-sync 0.10.1

A library for IMAP to maildir synchronization
Documentation
extern crate chrono;
extern crate imap;
extern crate lettre;
extern crate memory_logger;
// extern crate native_tls;

use imap::ImapConnection;
use tempfile::tempdir;

use memory_logger::blocking::MemoryLogger;

use std::io;

use vomit_sync::*;

const USER: &str = "vsync@localhost";

fn test_host() -> String {
    std::env::var("TEST_HOST").unwrap_or("127.0.0.1".to_string())
}

fn test_imaps_port() -> u16 {
    std::env::var("TEST_IMAPS_PORT")
        .unwrap_or("3143".to_string())
        .parse()
        .unwrap_or(3143)
}

fn clean_mailbox<T: io::Read + io::Write>(session: &mut imap::Session<T>) {
    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<Box<dyn ImapConnection>> {
    let host = test_host();
    let mut s = imap::ClientBuilder::new(&host, test_imaps_port())
        .mode(imap::ConnectionMode::Plaintext)
        .connect()
        .unwrap()
        .login(user, user)
        .unwrap();
    s.debug = true;
    clean_mailbox(&mut s);
    s
}

#[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 tmp_path = dir.path().to_owned();
    assert!(tmp_path.exists());

    let maildir = maildir::Maildir::from(tmp_path);
    maildir.create_dirs().unwrap();
    assert_eq!(maildir.count_new(), 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: lettre::Message = lettre::message::Message::builder()
        .from("sender@localhost".parse().unwrap())
        .to(USER.parse().unwrap())
        .subject("My second e-mail")
        .body("Hello world".to_string())
        .unwrap()
        .into();
    // append message
    c.append(mbox, &e.formatted()).finish().unwrap();

    let opts = SyncOptions {
        // The local maildir to sync to
        local: String::from(maildir.path().to_str().unwrap()),
        // The IMAPS URL to sync from
        remote: String::from(format!("{}:{}", test_host(), test_imaps_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 completely (insecure)
        disable_tls: true,
        // 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,
    };

    // Initial pull to get some local state
    pull(&opts).unwrap();

    let logs = logger.read();
    let text: &str = &logs;
    println!("{}", text);
    assert!(logs.contains("Discarding local state"));
    assert!(logs.contains("forces full resync"));
    assert!(logs.contains("full fetch for INBOX"));
    drop(logs);
    logger.clear();

    // Do another pull to verify that nothing happens
    pull(&opts).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();

    let maildir = maildir::Maildir::from(dir.path().to_owned());
    assert_eq!(maildir.count_new() + maildir.count_cur(), 1);

    // 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).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).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!(maildir.count_new() + maildir.count_cur(), 1);

    // Assert it's back in mailbox
    let inbox = c.search("ALL").unwrap();
    assert_eq!(inbox.len(), 1);

    // Store a new mail locally
    let mail_id = maildir.store_cur_with_flags(&e.formatted(), "S").unwrap();
    // and add "another" one remotely
    c.append(mbox, &e.formatted()).finish().unwrap();

    // sync both ways
    sync(&opts).unwrap();

    let logs = logger.read();
    let text: &str = &logs;
    println!("{}", text);
    drop(logs);
    logger.clear();

    // another sync should do nothing
    sync(&opts).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!(maildir.count_new() + maildir.count_cur(), 3);
    let inbox = c.uid_search("ALL").unwrap();
    assert_eq!(inbox.len(), 3);

    // Delete a mail locally
    maildir.delete(&mail_id).unwrap();

    // another sync should delete it remotely
    sync(&opts).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!(maildir.count_new() + maildir.count_cur(), 2);
    let inbox = c.uid_search("ALL").unwrap();
    assert_eq!(inbox.len(), 2);
}