From 1baeb6de05232250e643ed80c90959fe5aa34adf Mon Sep 17 00:00:00 2001 From: William Desportes Date: Tue, 23 Jul 2024 17:55:15 +0200 Subject: [PATCH 1/2] Add a function to store with previous unique settings --- src/lib.rs | 80 +++++++++++++++++++++++++++++++++----------------- tests/smoke.rs | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 39f89e9..0e82ee8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -671,22 +671,16 @@ impl Maildir { data: &[u8], flags: &str, ) -> std::result::Result { - self.store( - Subfolder::Cur, - data, - &format!( - "{}2,{}", - INFORMATIONAL_SUFFIX_SEPARATOR, - Self::normalize_flags(flags) - ), - ) + self.store(Subfolder::Cur, data, &Self::normalize_flags(flags)) } + // https://doc.dovecot.org/admin_manual/mailbox_formats/maildir/#maildir-filename-extensions + // The standard filename definition is: :2, fn store( &self, subfolder: Subfolder, data: &[u8], - info: &str, + flags: &str, ) -> std::result::Result { // try to get some uniquenes, as described at http://cr.yp.to/proto/maildir.html // dovecot and courier IMAP use .MP. for tmp-files and then @@ -706,13 +700,14 @@ impl Maildir { let mut tmppath = self.path.clone(); tmppath.push("tmp"); - let mut file; let mut secs; + let mut ts; let mut nanos; let mut counter; loop { - let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?; + let now = time::SystemTime::now(); + ts = now.duration_since(time::UNIX_EPOCH)?; secs = ts.as_secs(); nanos = ts.subsec_nanos(); counter = COUNTER.fetch_add(1, Ordering::SeqCst); @@ -724,9 +719,21 @@ impl Maildir { .create_new(true) .open(&tmppath) { - Ok(f) => { - file = f; - break; + Ok(file) => { + return self.store_with_unique_settings( + match subfolder { + Subfolder::New => "new", + Subfolder::Cur => "cur", + }, + &counter, + &now.duration_since(time::UNIX_EPOCH)?, + &pid, + &hostname, + &tmppath.into(), + &file, + data, + if flags.len() == 0 { None } else { Some(flags) }, + ); } Err(err) => { if err.kind() != ErrorKind::AlreadyExists { @@ -736,8 +743,21 @@ impl Maildir { } } } + } - /// At this point, `file` is our new file at `tmppath`. + pub fn store_with_unique_settings( + &self, + subfolder: &str, + sequence_number: &usize, + timestamp: &time::Duration, + pid: &u32, + hostname: &str, + tmp_file_path: &PathBuf, + mut tmp_file: &std::fs::File, + data: &[u8], + flags: Option<&str>, + ) -> std::result::Result { + /// At this point, `file` is our new file at `tmp_file_path`. /// If we leave the scope of this function prior to /// successfully writing the file to its final location, /// we need to ensure that we remove the temporary file. @@ -757,18 +777,15 @@ impl Maildir { // Ensure that we remove the temporary file on failure let mut unlink_guard = UnlinkOnError { - path_to_unlink: Some(tmppath.clone()), + path_to_unlink: Some(tmp_file_path.to_path_buf()), }; - file.write_all(data)?; - file.sync_all()?; + tmp_file.write_all(data)?; + tmp_file.sync_all()?; - let meta = file.metadata()?; + let meta = tmp_file.metadata()?; let mut newpath = self.path.clone(); - newpath.push(match subfolder { - Subfolder::New => "new", - Subfolder::Cur => "cur", - }); + newpath.push(subfolder); #[cfg(unix)] let dev = meta.dev(); @@ -785,10 +802,19 @@ impl Maildir { #[cfg(windows)] let size = meta.file_size(); - let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}"); - newpath.push(format!("{}{}", id, info)); + let secs = timestamp.as_secs(); + let nanos = timestamp.subsec_nanos(); + + let id = + format!("{secs}.#{sequence_number:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}"); + + let flags_str = match flags { + Some(flags) => format!("{}2,{}", INFORMATIONAL_SUFFIX_SEPARATOR, flags), + None => "".to_string(), + }; + newpath.push(format!("{}{}", id, flags_str)); - std::fs::rename(&tmppath, &newpath)?; + std::fs::rename(&tmp_file_path, &newpath)?; unlink_guard.path_to_unlink.take(); Ok(id) } diff --git a/tests/smoke.rs b/tests/smoke.rs index 0e12760..9207363 100644 --- a/tests/smoke.rs +++ b/tests/smoke.rs @@ -11,6 +11,7 @@ use std::fs; use std::os::unix::ffi::OsStrExt; #[cfg(windows)] use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::time; use mailparse::MailHeaderMap; use percent_encoding::percent_decode; @@ -333,6 +334,58 @@ fn check_store_new() { }); } +#[test] +fn check_store_with_unique_settings() { + let tmp_dir = tempdir().expect("could not create temporary directory"); + let mut tmp_file_path = tmp_dir.into_path(); + tmp_file_path.push(format!( + "maildir-temp-file.{}", + time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("Duration should work") + .as_secs() + )); + + let tmp_file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&tmp_file_path) + .expect("The tempĀ file should create"); + + with_maildir_empty("maildir3", |maildir| { + maildir.create_dirs().unwrap(); + + assert_eq!(maildir.count_new(), 0); + let mut id = maildir.store_with_unique_settings( + "new", + &1, + &time::Duration::new(1721749101, 266173535), + &12345, + "mail-server.intranet", + &tmp_file_path, + &tmp_file, + TEST_MAIL_BODY, + None, // No flags + ); + eprintln!("{:?}", id); + assert!(id.is_ok()); + let message_id = id.as_mut().unwrap(); + assert!(message_id.contains("1721749101.#1M266173535P12345V")); + // The Inode part is not checked + assert!(message_id.contains(".mail-server.intranet,S=900")); + assert_eq!(maildir.count_new(), 1); + + let id = id.unwrap(); + let msg = maildir.find(&id); + assert!(msg.is_some()); + + assert_eq!( + msg.unwrap().parsed().unwrap().get_body_raw().unwrap(), + b"Today is Boomtime, the 59th day of Discord in the YOLD 3183".as_ref() + ); + }); +} + #[test] fn check_store_cur() { with_maildir_empty("maildir2", |maildir| { From a4213b852a77988c16236d2ae9b51718926ef023 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Tue, 23 Jul 2024 18:21:24 +0200 Subject: [PATCH 2/2] Add tests for a message with flags --- tests/smoke.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/smoke.rs b/tests/smoke.rs index 9207363..f3fbdea 100644 --- a/tests/smoke.rs +++ b/tests/smoke.rs @@ -367,7 +367,7 @@ fn check_store_with_unique_settings() { TEST_MAIL_BODY, None, // No flags ); - eprintln!("{:?}", id); + assert!(id.is_ok()); let message_id = id.as_mut().unwrap(); assert!(message_id.contains("1721749101.#1M266173535P12345V")); @@ -386,6 +386,63 @@ fn check_store_with_unique_settings() { }); } +#[test] +fn check_store_with_unique_settings_and_flags() { + let tmp_dir = tempdir().expect("could not create temporary directory"); + let mut tmp_file_path = tmp_dir.into_path(); + tmp_file_path.push(format!( + "maildir-temp-file.{}", + time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("Duration should work") + .as_secs() + )); + + let tmp_file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&tmp_file_path) + .expect("The tempĀ file should create"); + + with_maildir_empty("maildir4", |maildir| { + maildir.create_dirs().unwrap(); + + assert_eq!(maildir.count_new(), 0); + let mut id = maildir.store_with_unique_settings( + "cur", + &1, + &time::Duration::new(1721749101, 266173535), + &12345, + "mail-server.intranet", + &tmp_file_path, + &tmp_file, + TEST_MAIL_BODY, + // https://doc.dovecot.org/admin_manual/mailbox_formats/maildir/#usage-of-timestamps + Some("STln"), + ); + + assert!(id.is_ok()); + let message_id = id.as_mut().unwrap(); + assert!(message_id.contains("1721749101.#1M266173535P12345V")); + // The Inode part is not checked + assert!(message_id.contains(".mail-server.intranet,S=900")); + // Does not contain flags + assert!(!message_id.contains(":2,STln")); + assert_eq!(maildir.count_cur(), 1); + + let id = id.unwrap(); + let msg = maildir.find(&id); + assert!(&msg.is_some()); + let mut final_message = msg.unwrap(); + assert_eq!(final_message.flags(), "STln"); + + assert_eq!( + final_message.parsed().unwrap().get_body_raw().unwrap(), + b"Today is Boomtime, the 59th day of Discord in the YOLD 3183".as_ref() + ); + }); +} + #[test] fn check_store_cur() { with_maildir_empty("maildir2", |maildir| {