extern crate dirs; extern crate tempfile; extern crate notmuch; extern crate gethostname; extern crate maildir; extern crate lettre; extern crate lettre_email; use std::ffi::OsStr; use std::io::{Result, Write}; use std::fs::{self, File}; use std::path::PathBuf; use tempfile::{tempdir, TempDir}; use std::process::Command; use maildir::Maildir; use lettre_email::{EmailBuilder, Header}; use lettre::SendableEmail; // A basic test interface to a valid maildir directory. // // This creates a valid maildir and provides a simple mechanism to // deliver test emails to it. It also writes a notmuch-config file // in the top of the maildir. pub struct MailBox { root_dir: TempDir, maildir: Maildir } impl MailBox { // Creates a new maildir fixture. Since this is only used for tests, // may just panic of something is wrong pub fn new() -> Self { let root_dir = tempdir().unwrap(); let root_path = root_dir.path().to_path_buf(); let tmp_path = root_path.join("tmp"); fs::create_dir(&tmp_path).unwrap(); let cfg_fname = root_path.join("notmuch-config"); let mut cfg_file = File::create(cfg_fname).unwrap(); write!( cfg_file, r#" [database] path={tmppath} hook_dir={tmppath}/hooks [user] name=Some Hacker primary_email=dst@example.com [new] tags=unread;inbox; ignore= [search] exclude_tags=deleted;spam; [maildir] synchronize_flags=true [crypto] gpg_path=gpg "#, tmppath = root_path.to_string_lossy() ) .unwrap(); let maildir = Maildir::from(root_path.to_path_buf()); maildir.create_dirs().unwrap(); std::fs::create_dir(root_path.join("hooks")).unwrap(); Self { root_dir, maildir } } /// Return a new unique message ID // fn next_msgid(&mut self) -> String{ // let hostname = gethostname::gethostname(); // let msgid = format!("{}@{}", self.idcount, hostname.to_string_lossy()); // self.idcount += 1; // msgid // } pub fn path(&self) -> PathBuf { self.root_dir.path().into() } /// Deliver a new mail message in the mbox. /// This does only adds the message to maildir, does not insert it /// into the notmuch database. /// returns a tuple of (msgid, pathname). pub fn deliver(&self, subject: Option, body: Option, to: Option, from: Option, headers: Vec<(String, String)>, is_new: bool, // Move to new dir or cur dir? _keywords: Option>, // List of keywords or labels seen: bool, // Seen flag (cur dir only) replied: bool, // Replied flag (cur dir only) flagged: bool) // Flagged flag (cur dir only) -> Result<(String, PathBuf)> { let mut builder = EmailBuilder::new() .subject(subject.unwrap_or_else(|| "Test mail".to_string())); if let Some(val) = body { builder = builder.text(val); } builder = builder.to(to.unwrap_or_else(|| "to@example.com".to_string())) .from(from.unwrap_or_else(|| "src@example.com".to_string())); for h in headers.into_iter(){ let hdr: Header = h.into(); builder = builder.header(hdr); } let msg:SendableEmail = builder.build().unwrap().into(); // not sure why lettre doesn't add the host suffix itself let msg_id = msg.message_id().to_string() + ".lettre@localhost"; let id = if is_new { self.maildir.store_new(&msg.message_to_string().unwrap().as_bytes()).unwrap() }else{ let mut flags = String::from(""); if flagged { flags += "F"; } if replied { flags += "R"; } if seen { flags += "S"; } println!("flags: {:?}", flags); let mid = self.maildir.store_cur_with_flags(&msg.message_to_string().unwrap().as_bytes(), flags.as_str()).unwrap(); // I have no idea what the reasoning for the :2 here is, but ok. format!("{}:2,{}", mid, flags) }; let mut msgpath = self.path(); msgpath = if is_new { msgpath.join("new") } else { msgpath.join("cur") }; msgpath = msgpath.join(&id); Ok((msg_id, msgpath)) } } impl Drop for MailBox { fn drop(&mut self) { } } #[derive(Clone, Debug)] pub struct NotmuchCommand { maildir_path: PathBuf } impl NotmuchCommand { /// Return a function which runs notmuch commands on our test maildir. /// /// This uses the notmuch-config file created by the ``maildir`` /// fixture. pub fn new(maildir_path: &PathBuf) -> Self { Self { maildir_path: maildir_path.clone() } } /// Run a notmuch comand. /// /// This function runs with a timeout error as many notmuch /// commands may block if multiple processes are trying to open /// the database in write-mode. It is all too easy to /// accidentally do this in the unittests. pub fn run(&self, args: I) -> Result<()> where I: IntoIterator, S: AsRef { let cfg_fname = self.maildir_path.join("notmuch-config"); Command::new("notmuch") .env("NOTMUCH_CONFIG", &cfg_fname) .env_remove("NOTMUCH_DATABASE") .env_remove("NOTMUCH_PROFILE") .env_remove("MAILDIR") .args(args) .status()?; Ok(()) } }