From c3503f09495ecb0c166ec02f979dd94d453aa79e Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Wed, 23 Aug 2023 11:20:45 -0700 Subject: [PATCH] maildir: import my fork into the repo I want to remove the mailparse dep from it. I could do that its own little repo, but it's more convenient to add it to our monorepo, and then we can make it depend on our new mailparsing crate. --- Cargo.lock | 34 +- crates/integration-tests/Cargo.toml | 2 +- crates/kumod/Cargo.toml | 2 +- crates/maildir/Cargo.toml | 30 + crates/maildir/LICENSE | 13 + crates/maildir/README.md | 25 + crates/maildir/examples/explain.rs | 35 + crates/maildir/src/lib.rs | 777 ++++++++++++++++++ .../maildir/cur/.dotfiles_should_be_ignored | 0 ...5.38518452d49213cb409aa1db32f53184%3A2%2CS | 31 + .../maildir/new/.dotfiles_should_be_ignored | 0 ...463941010.5f7fa6dd4922c183dc457d033deee9d7 | 31 + .../testdata/submaildirs/..Subdir3/.gitkeep | 0 .../testdata/submaildirs/.Subdir1/.gitkeep | 0 .../testdata/submaildirs/.Subdir2/.gitkeep | 0 .../maildir/testdata/submaildirs/cur/.gitkeep | 0 .../maildir/testdata/submaildirs/new/.gitkeep | 0 crates/maildir/tests/smoke.rs | 326 ++++++++ 18 files changed, 1296 insertions(+), 10 deletions(-) create mode 100644 crates/maildir/Cargo.toml create mode 100644 crates/maildir/LICENSE create mode 100644 crates/maildir/README.md create mode 100644 crates/maildir/examples/explain.rs create mode 100644 crates/maildir/src/lib.rs create mode 100644 crates/maildir/testdata/maildir/cur/.dotfiles_should_be_ignored create mode 100644 crates/maildir/testdata/maildir/cur/1463868505.38518452d49213cb409aa1db32f53184%3A2%2CS create mode 100644 crates/maildir/testdata/maildir/new/.dotfiles_should_be_ignored create mode 100644 crates/maildir/testdata/maildir/new/1463941010.5f7fa6dd4922c183dc457d033deee9d7 create mode 100644 crates/maildir/testdata/submaildirs/..Subdir3/.gitkeep create mode 100644 crates/maildir/testdata/submaildirs/.Subdir1/.gitkeep create mode 100644 crates/maildir/testdata/submaildirs/.Subdir2/.gitkeep create mode 100644 crates/maildir/testdata/submaildirs/cur/.gitkeep create mode 100644 crates/maildir/testdata/submaildirs/new/.gitkeep create mode 100644 crates/maildir/tests/smoke.rs diff --git a/Cargo.lock b/Cargo.lock index 82cb6ba6..de2af4d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "751bbd7d440576066233e740576f1b31fdc6ab86cfabfbd48c548de77eca73e4" dependencies = [ "amq-protocol-types", - "percent-encoding", + "percent-encoding 2.3.0", "url", ] @@ -350,7 +350,7 @@ dependencies = [ "matchit", "memchr", "mime", - "percent-encoding", + "percent-encoding 2.3.0", "pin-project-lite", "rustversion", "serde", @@ -1561,7 +1561,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "percent-encoding", + "percent-encoding 2.3.0", ] [[package]] @@ -2745,10 +2745,13 @@ dependencies = [ [[package]] name = "maildir" version = "0.6.3" -source = "git+https://github.com/wez/maildir.git?rev=898d604fec05bfcb5af52f3960c4b07028faad39#898d604fec05bfcb5af52f3960c4b07028faad39" dependencies = [ "gethostname 0.2.3", "mailparse", + "memmap2", + "percent-encoding 1.0.1", + "tempfile", + "walkdir", ] [[package]] @@ -2819,6 +2822,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memmem" version = "0.1.1" @@ -3553,6 +3565,12 @@ dependencies = [ "base64ct", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.3.0" @@ -4067,7 +4085,7 @@ dependencies = [ "combine", "crc16", "itoa", - "percent-encoding", + "percent-encoding 2.3.0", "r2d2", "rand", "ryu", @@ -4178,7 +4196,7 @@ dependencies = [ "log", "mime", "once_cell", - "percent-encoding", + "percent-encoding 2.3.0", "pin-project-lite", "rustls", "rustls-pemfile", @@ -5334,7 +5352,7 @@ dependencies = [ "http-body", "hyper", "hyper-timeout", - "percent-encoding", + "percent-encoding 2.3.0", "pin-project", "prost", "tokio", @@ -5670,7 +5688,7 @@ checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", - "percent-encoding", + "percent-encoding 2.3.0", "serde", ] diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index 15f9b903..1885c06c 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -14,7 +14,7 @@ kumo-api-types = {path="../kumo-api-types"} kumo-log-types = {path="../kumo-log-types"} lipsum = "0.8" mail-builder = "0.2" -maildir = {git="https://github.com/wez/maildir.git", rev="898d604fec05bfcb5af52f3960c4b07028faad39"} +maildir = {path="../maildir"} mailparse = "0.14" nix = {version="0.26", features=["signal"]} rfc5321 = {path="../rfc5321"} diff --git a/crates/kumod/Cargo.toml b/crates/kumod/Cargo.toml index ba09b377..57d15738 100644 --- a/crates/kumod/Cargo.toml +++ b/crates/kumod/Cargo.toml @@ -37,7 +37,7 @@ kumo-server-runtime = {path="../kumo-server-runtime"} lazy_static = "1.4" lruttl = {path="../lruttl"} mail-builder = "0.2" -maildir = {git="https://github.com/wez/maildir.git", rev="898d604fec05bfcb5af52f3960c4b07028faad39"} +maildir = {path="../maildir"} memchr = "2.5" message = {path="../message"} metrics = "0.20" diff --git a/crates/maildir/Cargo.toml b/crates/maildir/Cargo.toml new file mode 100644 index 00000000..3b6eb130 --- /dev/null +++ b/crates/maildir/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "maildir" +version = "0.6.3" +authors = ["Kartikaya Gupta"] +edition = "2018" +license = "0BSD" + +description = "A simple library for maildir manipulation" +homepage = "https://github.com/staktrace/maildir/blob/master/README.md" +repository = "https://github.com/staktrace/maildir" +readme = "README.md" +keywords = ["maildir", "email", "rfc822", "mime"] +categories = ["email", "filesystem"] +exclude = [".gitignore", ".github/**"] + +[badges] +maintenance = { status = "passively-maintained" } + +[dependencies] +mailparse = "0.14" +gethostname = "0.2.3" +memmap2 = { version = "0.5.8", optional = true } + +[features] +mmap = ["memmap2"] + +[dev-dependencies] +tempfile = "3.0.8" +walkdir = "2.2.7" +percent-encoding = "1.0.1" diff --git a/crates/maildir/LICENSE b/crates/maildir/LICENSE new file mode 100644 index 00000000..7b1f54d3 --- /dev/null +++ b/crates/maildir/LICENSE @@ -0,0 +1,13 @@ +Copyright (C) 2019 by Kartikaya Gupta + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/crates/maildir/README.md b/crates/maildir/README.md new file mode 100644 index 00000000..c2a353e1 --- /dev/null +++ b/crates/maildir/README.md @@ -0,0 +1,25 @@ +maildir +=== +![Build Status](https://github.com/staktrace/maildir/actions/workflows/test.yml/badge.svg) +[![Crate](https://img.shields.io/crates/v/maildir.svg)](https://crates.io/crates/maildir) + +A simple library to deal with maildir folders + +API +--- +The primary entry point for this library is the Maildir structure, which can be created from a path, like so: + +```rust + let maildir = Maildir::from("path/to/maildir"); +``` + +The Maildir structure then has functions that can be used to access and modify mail files. + +Documentation +--- +See the rustdoc at [docs.rs](https://docs.rs/maildir/). + +Support maildir +--- +If you want to support development of `maildir`, please do so by donating your money, time, and/or energy to fighting climate change. +A quick and easy way is to send a donation to [Replant.ca Environmental](http://www.replant-environmental.ca/donate.html), where every dollar gets a tree planted! diff --git a/crates/maildir/examples/explain.rs b/crates/maildir/examples/explain.rs new file mode 100644 index 00000000..67728db5 --- /dev/null +++ b/crates/maildir/examples/explain.rs @@ -0,0 +1,35 @@ +use maildir::MailEntry; +use maildir::Maildir; +use std::io; + +fn list_mail(mail: MailEntry) { + println!("Path: {}", mail.path().display()); + println!("ID: {}", mail.id()); + println!("Flags: {}", mail.flags()); + println!("is_draft: {}", mail.is_draft()); + println!("is_flagged: {}", mail.is_flagged()); + println!("is_passed: {}", mail.is_passed()); + println!("is_replied: {}", mail.is_replied()); + println!("is_seen: {}", mail.is_seen()); + println!("is_trashed: {}", mail.is_trashed()); +} + +fn process_maildirs(maildirs: impl IntoIterator) -> Result<(), io::Error> { + maildirs.into_iter().try_for_each(|mdir| { + mdir.list_new() + .chain(mdir.list_cur()) + .map(|r| r.map(list_mail)) + .collect::>() + }) +} + +fn main() { + let rc = match process_maildirs(std::env::args().skip(1).map(Into::into)) { + Err(e) => { + eprintln!("Error: {:?}", e); + 1 + } + Ok(_) => 0, + }; + std::process::exit(rc); +} diff --git a/crates/maildir/src/lib.rs b/crates/maildir/src/lib.rs new file mode 100644 index 00000000..4b9d6017 --- /dev/null +++ b/crates/maildir/src/lib.rs @@ -0,0 +1,777 @@ +#[cfg(feature = "mmap")] +extern crate memmap2; + +use std::error; +use std::fmt; +use std::fs; +use std::io::prelude::*; +use std::io::ErrorKind; +use std::ops::Deref; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); + +use mailparse::*; + +#[cfg(unix)] +const INFORMATIONAL_SUFFIX_SEPARATOR: &str = ":"; +#[cfg(windows)] +const INFORMATIONAL_SUFFIX_SEPARATOR: &str = ";"; + +#[derive(Debug)] +pub enum MailEntryError { + IOError(std::io::Error), + ParseError(MailParseError), + DateError(&'static str), +} + +impl fmt::Display for MailEntryError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + MailEntryError::IOError(ref err) => write!(f, "IO error: {}", err), + MailEntryError::ParseError(ref err) => write!(f, "Parse error: {}", err), + MailEntryError::DateError(ref msg) => write!(f, "Date error: {}", msg), + } + } +} + +impl error::Error for MailEntryError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + MailEntryError::IOError(ref err) => Some(err), + MailEntryError::ParseError(ref err) => Some(err), + MailEntryError::DateError(_) => None, + } + } +} + +impl From for MailEntryError { + fn from(err: std::io::Error) -> MailEntryError { + MailEntryError::IOError(err) + } +} + +impl From for MailEntryError { + fn from(err: MailParseError) -> MailEntryError { + MailEntryError::ParseError(err) + } +} + +impl From<&'static str> for MailEntryError { + fn from(err: &'static str) -> MailEntryError { + MailEntryError::DateError(err) + } +} + +enum MailData { + None, + #[cfg(not(feature = "mmap"))] + Bytes(Vec), + #[cfg(feature = "mmap")] + File(memmap2::Mmap), +} + +impl MailData { + fn is_none(&self) -> bool { + match self { + MailData::None => true, + _ => false, + } + } +} + +/// This struct represents a single email message inside +/// the maildir. Creation of the struct does not automatically +/// load the content of the email file into memory - however, +/// that may happen upon calling functions that require parsing +/// the email. +pub struct MailEntry { + id: String, + flags: String, + path: PathBuf, + data: MailData, +} + +impl MailEntry { + pub fn id(&self) -> &str { + &self.id + } + + fn read_data(&mut self) -> std::io::Result<()> { + if self.data.is_none() { + #[cfg(feature = "mmap")] + { + let f = fs::File::open(&self.path)?; + let mmap = unsafe { memmap2::MmapOptions::new().map(&f)? }; + self.data = MailData::File(mmap); + } + + #[cfg(not(feature = "mmap"))] + { + let mut f = fs::File::open(&self.path)?; + let mut d = Vec::::new(); + f.read_to_end(&mut d)?; + self.data = MailData::Bytes(d); + } + } + Ok(()) + } + + pub fn parsed(&mut self) -> Result { + self.read_data()?; + match self.data { + MailData::None => panic!("read_data should have returned an Err!"), + #[cfg(not(feature = "mmap"))] + MailData::Bytes(ref b) => parse_mail(b).map_err(MailEntryError::ParseError), + #[cfg(feature = "mmap")] + MailData::File(ref m) => parse_mail(m).map_err(MailEntryError::ParseError), + } + } + + pub fn headers(&mut self) -> Result, MailEntryError> { + self.read_data()?; + let headers = match self.data { + MailData::None => panic!("read_data should have returned an Err!"), + #[cfg(not(feature = "mmap"))] + MailData::Bytes(ref b) => parse_headers(b), + #[cfg(feature = "mmap")] + MailData::File(ref m) => parse_headers(m), + }; + headers.map(|(v, _)| v).map_err(MailEntryError::ParseError) + } + + pub fn received(&mut self) -> Result { + self.read_data()?; + let headers = self.headers()?; + let received = headers.get_first_value("Received"); + match received { + Some(v) => v + .rsplit(';') + .nth(0) + .ok_or_else(|| MailEntryError::DateError("Unable to split Received header")) + .and_then(|ts| dateparse(ts).map_err(MailEntryError::from)), + None => Err("No Received header found")?, + } + } + + pub fn date(&mut self) -> Result { + self.read_data()?; + let headers = self.headers()?; + let date = headers.get_first_value("Date"); + match date { + Some(ts) => dateparse(&ts).map_err(MailEntryError::from), + None => Err("No Date header found")?, + } + } + + pub fn flags(&self) -> &str { + &self.flags + } + + pub fn is_draft(&self) -> bool { + self.flags.contains('D') + } + + pub fn is_flagged(&self) -> bool { + self.flags.contains('F') + } + + pub fn is_passed(&self) -> bool { + self.flags.contains('P') + } + + pub fn is_replied(&self) -> bool { + self.flags.contains('R') + } + + pub fn is_seen(&self) -> bool { + self.flags.contains('S') + } + + pub fn is_trashed(&self) -> bool { + self.flags.contains('T') + } + + pub fn path(&self) -> &PathBuf { + &self.path + } +} + +enum Subfolder { + New, + Cur, +} + +/// An iterator over the email messages in a particular +/// maildir subfolder (either `cur` or `new`). This iterator +/// produces a `std::io::Result`, which can be an +/// `Err` if an error was encountered while trying to read +/// file system properties on a particular entry, or if an +/// invalid file was found in the maildir. Files starting with +/// a dot (.) character in the maildir folder are ignored. +pub struct MailEntries { + path: PathBuf, + subfolder: Subfolder, + readdir: Option, +} + +impl MailEntries { + fn new(path: PathBuf, subfolder: Subfolder) -> MailEntries { + MailEntries { + path, + subfolder, + readdir: None, + } + } +} + +impl Iterator for MailEntries { + type Item = std::io::Result; + + fn next(&mut self) -> Option> { + if self.readdir.is_none() { + let mut dir_path = self.path.clone(); + dir_path.push(match self.subfolder { + Subfolder::New => "new", + Subfolder::Cur => "cur", + }); + self.readdir = match fs::read_dir(dir_path) { + Err(_) => return None, + Ok(v) => Some(v), + }; + } + + loop { + // we need to skip over files starting with a '.' + let dir_entry = self.readdir.iter_mut().next().unwrap().next(); + let result = dir_entry.map(|e| { + let entry = e?; + let filename = String::from(entry.file_name().to_string_lossy().deref()); + if filename.starts_with('.') { + return Ok(None); + } + let (id, flags) = match self.subfolder { + Subfolder::New => (Some(filename.as_str()), Some("")), + Subfolder::Cur => { + let delim = format!("{}2,", INFORMATIONAL_SUFFIX_SEPARATOR); + let mut iter = filename.split(&delim); + (iter.next(), iter.next()) + } + }; + if id.is_none() || flags.is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Non-maildir file found in maildir", + )); + } + Ok(Some(MailEntry { + id: String::from(id.unwrap()), + flags: String::from(flags.unwrap()), + path: entry.path(), + data: MailData::None, + })) + }); + return match result { + None => None, + Some(Err(e)) => Some(Err(e)), + Some(Ok(None)) => continue, + Some(Ok(Some(v))) => Some(Ok(v)), + }; + } + } +} + +#[derive(Debug)] +pub enum MaildirError { + Io(std::io::Error), + Utf8(std::str::Utf8Error), + Time(std::time::SystemTimeError), +} + +impl fmt::Display for MaildirError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use MaildirError::*; + + match *self { + Io(ref e) => write!(f, "IO Error: {}", e), + Utf8(ref e) => write!(f, "UTF8 Encoding Error: {}", e), + Time(ref e) => write!(f, "Time Error: {}", e), + } + } +} + +impl error::Error for MaildirError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + use MaildirError::*; + + match *self { + Io(ref e) => Some(e), + Utf8(ref e) => Some(e), + Time(ref e) => Some(e), + } + } +} + +impl From for MaildirError { + fn from(e: std::io::Error) -> MaildirError { + MaildirError::Io(e) + } +} +impl From for MaildirError { + fn from(e: std::str::Utf8Error) -> MaildirError { + MaildirError::Utf8(e) + } +} +impl From for MaildirError { + fn from(e: std::time::SystemTimeError) -> MaildirError { + MaildirError::Time(e) + } +} + +/// An iterator over the maildir subdirectories. This iterator +/// produces a `std::io::Result`, which can be an +/// `Err` if an error was encountered while trying to read +/// file system properties on a particular entry. Only +/// subdirectories starting with a single period are included. +pub struct MaildirEntries { + path: PathBuf, + readdir: Option, +} + +impl MaildirEntries { + fn new(path: PathBuf) -> MaildirEntries { + MaildirEntries { + path, + readdir: None, + } + } +} + +impl Iterator for MaildirEntries { + type Item = std::io::Result; + + fn next(&mut self) -> Option> { + if self.readdir.is_none() { + self.readdir = match fs::read_dir(&self.path) { + Err(_) => return None, + Ok(v) => Some(v), + }; + } + + loop { + let dir_entry = self.readdir.iter_mut().next().unwrap().next(); + let result = dir_entry.map(|e| { + let entry = e?; + + // a dir name should start by one single period + let filename = String::from(entry.file_name().to_string_lossy().deref()); + if !filename.starts_with('.') || filename.starts_with("..") { + return Ok(None); + } + + // the entry should be a directory + let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or_default(); + if !is_dir { + return Ok(None); + } + + Ok(Some(Maildir { + path: self.path.join(filename), + })) + }); + + return match result { + None => None, + Some(Err(e)) => Some(Err(e)), + Some(Ok(None)) => continue, + Some(Ok(Some(v))) => Some(Ok(v)), + }; + } + } +} + +/// The main entry point for this library. This struct can be +/// instantiated from a path using the `from` implementations. +/// The path passed in to the `from` should be the root of the +/// maildir (the folder containing `cur`, `new`, and `tmp`). +pub struct Maildir { + path: PathBuf, +} + +impl Maildir { + /// Returns the path of the maildir base folder. + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the number of messages found inside the `new` + /// maildir folder. + pub fn count_new(&self) -> usize { + self.list_new().count() + } + + /// Returns the number of messages found inside the `cur` + /// maildir folder. + pub fn count_cur(&self) -> usize { + self.list_cur().count() + } + + /// Returns an iterator over the messages inside the `new` + /// maildir folder. The order of messages in the iterator + /// is not specified, and is not guaranteed to be stable + /// over multiple invocations of this method. + pub fn list_new(&self) -> MailEntries { + MailEntries::new(self.path.clone(), Subfolder::New) + } + + /// Returns an iterator over the messages inside the `cur` + /// maildir folder. The order of messages in the iterator + /// is not specified, and is not guaranteed to be stable + /// over multiple invocations of this method. + pub fn list_cur(&self) -> MailEntries { + MailEntries::new(self.path.clone(), Subfolder::Cur) + } + + /// Returns an iterator over the maildir subdirectories. + /// The order of subdirectories in the iterator + /// is not specified, and is not guaranteed to be stable + /// over multiple invocations of this method. + pub fn list_subdirs(&self) -> MaildirEntries { + MaildirEntries::new(self.path.clone()) + } + + /// Moves a message from the `new` maildir folder to the + /// `cur` maildir folder. The id passed in should be + /// obtained from the iterator produced by `list_new`. + pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> { + self.move_new_to_cur_with_flags(id, "") + } + + /// Moves a message from the `new` maildir folder to the `cur` maildir folder, and sets the + /// given flags. The id passed in should be obtained from the iterator produced by `list_new`. + /// + /// The possible flags are described e.g. at or + /// . + pub fn move_new_to_cur_with_flags(&self, id: &str, flags: &str) -> std::io::Result<()> { + let src = self.path.join("new").join(id); + let dst = self.path.join("cur").join(format!( + "{}{}2,{}", + id, + INFORMATIONAL_SUFFIX_SEPARATOR, + Self::normalize_flags(flags) + )); + fs::rename(src, dst) + } + + /// Copies a message from the current maildir to the targetted maildir. + pub fn copy_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> { + let entry = self.find(id).ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found") + })?; + let filename = entry.path().file_name().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid mail entry file name", + ) + })?; + + let src_path = entry.path(); + let dst_path = target.path().join("cur").join(filename); + if src_path == &dst_path { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Target maildir needs to be different from the source", + )); + } + + fs::copy(src_path, dst_path)?; + Ok(()) + } + + /// Moves a message from the current maildir to the targetted maildir. + pub fn move_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> { + let entry = self.find(id).ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found") + })?; + let filename = entry.path().file_name().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid mail entry file name", + ) + })?; + fs::rename(entry.path(), target.path().join("cur").join(filename))?; + Ok(()) + } + + /// Tries to find the message with the given id in the + /// maildir. This searches both the `new` and the `cur` + /// folders. + pub fn find(&self, id: &str) -> Option { + let filter = |entry: &std::io::Result| match *entry { + Err(_) => false, + Ok(ref e) => e.id() == id, + }; + + self.list_new() + .find(&filter) + .or_else(|| self.list_cur().find(&filter)) + .map(|e| e.unwrap()) + } + + fn normalize_flags(flags: &str) -> String { + let mut flag_chars = flags.chars().collect::>(); + flag_chars.sort(); + flag_chars.dedup(); + flag_chars.into_iter().collect() + } + + fn update_flags(&self, id: &str, flag_op: F) -> std::io::Result<()> + where + F: Fn(&str) -> String, + { + let filter = |entry: &std::io::Result| match *entry { + Err(_) => false, + Ok(ref e) => e.id() == id, + }; + + match self.list_cur().find(&filter).map(|e| e.unwrap()) { + Some(m) => { + let src = m.path(); + let mut dst = m.path().clone(); + dst.pop(); + dst.push(format!( + "{}{}2,{}", + m.id(), + INFORMATIONAL_SUFFIX_SEPARATOR, + flag_op(m.flags()) + )); + fs::rename(src, dst) + } + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Mail entry not found", + )), + } + } + + /// Updates the flags for the message with the given id in the + /// maildir. This only searches the `cur` folder, because that's + /// the folder where messages have flags. Returns an error if the + /// message was not found. All existing flags are overwritten with + /// the new flags provided. + pub fn set_flags(&self, id: &str, flags: &str) -> std::io::Result<()> { + self.update_flags(id, |_old_flags| Self::normalize_flags(flags)) + } + + /// Adds the given flags to the message with the given id in the maildir. + /// This only searches the `cur` folder, because that's the folder where + /// messages have flags. Returns an error if the message was not found. + /// Flags are deduplicated, so setting a already-set flag has no effect. + pub fn add_flags(&self, id: &str, flags: &str) -> std::io::Result<()> { + let flag_merge = |old_flags: &str| { + let merged = String::from(old_flags) + flags; + Self::normalize_flags(&merged) + }; + self.update_flags(id, &flag_merge) + } + + /// Removes the given flags to the message with the given id in the maildir. + /// This only searches the `cur` folder, because that's the folder where + /// messages have flags. Returns an error if the message was not found. + /// If the message doesn't have the flag(s) to be removed, those flags are + /// ignored. + pub fn remove_flags(&self, id: &str, flags: &str) -> std::io::Result<()> { + let flag_strip = + |old_flags: &str| old_flags.chars().filter(|c| !flags.contains(*c)).collect(); + self.update_flags(id, &flag_strip) + } + + /// Deletes the message with the given id in the maildir. + /// This searches both the `new` and the `cur` folders, + /// and deletes the file from the filesystem. Returns an + /// error if no message was found with the given id. + pub fn delete(&self, id: &str) -> std::io::Result<()> { + match self.find(id) { + Some(m) => fs::remove_file(m.path()), + None => Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Mail entry not found", + )), + } + } + + /// Creates all neccessary directories if they don't exist yet. It is the library user's + /// responsibility to call this before using `store_new`. + pub fn create_dirs(&self) -> std::io::Result<()> { + let mut path = self.path.clone(); + for d in &["cur", "new", "tmp"] { + path.push(d); + fs::create_dir_all(path.as_path())?; + path.pop(); + } + Ok(()) + } + + /// Stores the given message data as a new message file in the Maildir `new` folder. Does not + /// create the neccessary directories, so if in doubt call `create_dirs` before using + /// `store_new`. + /// Returns the Id of the inserted message on success. + pub fn store_new(&self, data: &[u8]) -> std::result::Result { + self.store(Subfolder::New, data, "") + } + + /// Stores the given message data as a new message file in the Maildir `cur` folder, adding the + /// given `flags` to it. The possible flags are explained e.g. at + /// or . + /// Returns the Id of the inserted message on success. + pub fn store_cur_with_flags( + &self, + data: &[u8], + flags: &str, + ) -> std::result::Result { + self.store( + Subfolder::Cur, + data, + &format!( + "{}2,{}", + INFORMATIONAL_SUFFIX_SEPARATOR, + Self::normalize_flags(flags) + ), + ) + } + + fn store( + &self, + subfolder: Subfolder, + data: &[u8], + info: &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 + // move to .MPVI.,S= when moving + // to new dir. see for example http://www.courier-mta.org/maildir.html. + let pid = std::process::id(); + let hostname = gethostname::gethostname() + .into_string() + // the hostname is always ASCII in order to be a valid DNS + // name, so into_string() will always succeed. The error case + // here is to satisfy the compiler which doesn't know this. + .unwrap_or_else(|_| "localhost".to_string()); + + // loop when conflicting filenames occur, as described at + // http://www.courier-mta.org/maildir.html + // this assumes that pid and hostname don't change. + let mut tmppath = self.path.clone(); + tmppath.push("tmp"); + + let mut file; + let mut secs; + let mut nanos; + let mut counter; + + loop { + let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?; + secs = ts.as_secs(); + nanos = ts.subsec_nanos(); + counter = COUNTER.fetch_add(1, Ordering::SeqCst); + + tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}")); + + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&tmppath) + { + Ok(f) => { + file = f; + break; + } + Err(err) => { + if err.kind() != ErrorKind::AlreadyExists { + return Err(err.into()); + } + tmppath.pop(); + } + } + } + + /// At this point, `file` is our new file at `tmppath`. + /// 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. + /// This struct takes care of that detail. + struct UnlinkOnError { + path_to_unlink: Option, + } + + impl Drop for UnlinkOnError { + fn drop(&mut self) { + if let Some(path) = self.path_to_unlink.take() { + // Best effort to remove it + std::fs::remove_file(path).ok(); + } + } + } + + // Ensure that we remove the temporary file on failure + let mut unlink_guard = UnlinkOnError { + path_to_unlink: Some(tmppath.clone()), + }; + + file.write_all(data)?; + file.sync_all()?; + + let meta = file.metadata()?; + let mut newpath = self.path.clone(); + newpath.push(match subfolder { + Subfolder::New => "new", + Subfolder::Cur => "cur", + }); + + #[cfg(unix)] + let dev = meta.dev(); + #[cfg(windows)] + let dev: u64 = 0; + + #[cfg(unix)] + let ino = meta.ino(); + #[cfg(windows)] + let ino: u64 = 0; + + #[cfg(unix)] + let size = meta.size(); + #[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)); + + std::fs::rename(&tmppath, &newpath)?; + unlink_guard.path_to_unlink.take(); + Ok(id) + } +} + +impl From for Maildir { + fn from(p: PathBuf) -> Maildir { + Maildir { path: p } + } +} + +impl From for Maildir { + fn from(s: String) -> Maildir { + Maildir::from(PathBuf::from(s)) + } +} + +impl<'a> From<&'a str> for Maildir { + fn from(s: &str) -> Maildir { + Maildir::from(PathBuf::from(s)) + } +} diff --git a/crates/maildir/testdata/maildir/cur/.dotfiles_should_be_ignored b/crates/maildir/testdata/maildir/cur/.dotfiles_should_be_ignored new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/testdata/maildir/cur/1463868505.38518452d49213cb409aa1db32f53184%3A2%2CS b/crates/maildir/testdata/maildir/cur/1463868505.38518452d49213cb409aa1db32f53184%3A2%2CS new file mode 100644 index 00000000..ca517000 --- /dev/null +++ b/crates/maildir/testdata/maildir/cur/1463868505.38518452d49213cb409aa1db32f53184%3A2%2CS @@ -0,0 +1,31 @@ +Return-Path: +X-Original-To: test.foobar@example.com +Delivered-To: x11700823@homiemail-mx7.g.dreamhost.com +Received: from homiemail-a22.g.dreamhost.com (agjbgdcfdaaf.dreamhost.com [69.163.253.5]) + (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) + (No client certificate requested) + by homiemail-mx7.g.dreamhost.com (Postfix) with ESMTPS id 266801B5A25A + for ; Sat, 21 May 2016 15:08:27 -0700 (PDT) +Received: from homiemail-a22.g.dreamhost.com (localhost [127.0.0.1]) + by homiemail-a22.g.dreamhost.com (Postfix) with ESMTP id D754B114066 + for ; Sat, 21 May 2016 15:08:26 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=example.com; h= + mime-version:from:to:subject:date:content-type:message-id; s= + example.com; bh=GICvp8PrP/1WRMKD4SG0HV8hzq0=; b=hGorQMWIxA6tFt + +h5bIG81B9AHK3kTpAhHrRvyy01Xnbkai4vAaEMWn2pXzb1KkdxKquaZhnEagLrA + 6OiVHlYHbM+y6USAw1+O6/AqB/AkctAZiaHXEQEtefBIWK1zzD9jHnPF1C5Ylb6K + hdMCOp6xlDDqNLXEBjIkfmFjeXU1U= +Received: from localhost (apache2-quack.mug.dreamhost.com [208.113.163.206]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (No client certificate requested) + by homiemail-a22.g.dreamhost.com (Postfix) with ESMTPSA id 9AEBE114065 + for ; Sat, 21 May 2016 15:08:26 -0700 (PDT) +MIME-Version: 1.0 +From: Kartikaya Gupta +To: test.foobar@example.com +Subject: test +Date: Sat, 21 May 2016 22:08:25 +0000 +Content-Type: text/plain; charset=utf-8 +Message-Id: <20160521220826.9AEBE114065@homiemail-a22.g.dreamhost.com> + +Roundtrip diff --git a/crates/maildir/testdata/maildir/new/.dotfiles_should_be_ignored b/crates/maildir/testdata/maildir/new/.dotfiles_should_be_ignored new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/testdata/maildir/new/1463941010.5f7fa6dd4922c183dc457d033deee9d7 b/crates/maildir/testdata/maildir/new/1463941010.5f7fa6dd4922c183dc457d033deee9d7 new file mode 100644 index 00000000..0002c2c6 --- /dev/null +++ b/crates/maildir/testdata/maildir/new/1463941010.5f7fa6dd4922c183dc457d033deee9d7 @@ -0,0 +1,31 @@ +Return-Path: +X-Original-To: test.foobar@example.com +Delivered-To: x11700823@homiemail-mx8.g.dreamhost.com +Received: from homiemail-a15.g.dreamhost.com (agjbgdcfdaaf.dreamhost.com [69.163.253.5]) + (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) + (No client certificate requested) + by homiemail-mx8.g.dreamhost.com (Postfix) with ESMTPS id 7DEB6A010C + for ; Sun, 22 May 2016 11:16:51 -0700 (PDT) +Received: from homiemail-a15.g.dreamhost.com (localhost [127.0.0.1]) + by homiemail-a15.g.dreamhost.com (Postfix) with ESMTP id 3016776C06B; + Sun, 22 May 2016 11:16:51 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha1; c=relaxed; d=example.com; h= + mime-version:from:to:subject:date:content-type:message-id; s= + example.com; bh=OtypsA3TTYXz2zqEj+PynojfmXg=; b=SDmJv4I7Cq/qtN + eLA2FWO0PW59gmRw7JPjnQ8lRLrB6DaR7REHkfbKOurvMmri6WFlm59kS52zS0Xm + KDdhEO3tBFIGTtytiZAI9W033LHb8fZiwQ8W7O3ssSWUBR3kV6fDXjB4/uSXt+kU + H3aU9QSlHlPohrye0oGaSB5sl838k= +Received: from localhost (apache2-quack.mug.dreamhost.com [208.113.163.206]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (No client certificate requested) + by homiemail-a15.g.dreamhost.com (Postfix) with ESMTPSA id D149A76C069; + Sun, 22 May 2016 11:16:50 -0700 (PDT) +MIME-Version: 1.0 +From: Kartikaya Gupta +To: test.foobar@example.com +Subject: test +Date: Sun, 22 May 2016 18:16:50 +0000 +Content-Type: text/plain; charset=utf-8 +Message-Id: <20160522181650.D149A76C069@homiemail-a15.g.dreamhost.com> + +Ignore this :) diff --git a/crates/maildir/testdata/submaildirs/..Subdir3/.gitkeep b/crates/maildir/testdata/submaildirs/..Subdir3/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/testdata/submaildirs/.Subdir1/.gitkeep b/crates/maildir/testdata/submaildirs/.Subdir1/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/testdata/submaildirs/.Subdir2/.gitkeep b/crates/maildir/testdata/submaildirs/.Subdir2/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/testdata/submaildirs/cur/.gitkeep b/crates/maildir/testdata/submaildirs/cur/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/testdata/submaildirs/new/.gitkeep b/crates/maildir/testdata/submaildirs/new/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/maildir/tests/smoke.rs b/crates/maildir/tests/smoke.rs new file mode 100644 index 00000000..4f982cf3 --- /dev/null +++ b/crates/maildir/tests/smoke.rs @@ -0,0 +1,326 @@ +use maildir::*; + +#[cfg(unix)] +use std::borrow::Cow; +#[cfg(unix)] +use std::ffi::OsStr; +#[cfg(windows)] +use std::ffi::OsString; +use std::fs; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(windows)] +use std::os::windows::ffi::{OsStrExt, OsStringExt}; + +use mailparse::MailHeaderMap; +use percent_encoding::percent_decode; +use tempfile::tempdir; +use walkdir::WalkDir; + +static TESTDATA_DIR: &str = "testdata"; +static MAILDIR_NAME: &str = "maildir"; +static SUBMAILDIRS_NAME: &str = "submaildirs"; + +// `cargo package` doesn't package files with certain characters, such as +// colons, in the name, so we percent-decode the file names when copying the +// data for the tests. +// This code can likely be improved (for correctness, particularly on Windows) +// but there's no good docs on what `cargo package` does percent-encoding for, +// and how it deals with multibyte characters in filenames. In practice this +// code works fine for this crate, because we have a restricted set of ASCII +// characters in the filenames. +fn with_maildir(name: &str, func: F) +where + F: FnOnce(Maildir), +{ + let tmp_dir = tempdir().expect("could not create temporary directory"); + let tmp_path = tmp_dir.path(); + for entry in WalkDir::new(TESTDATA_DIR) { + let entry = entry.expect("directory walk error"); + let relative = entry.path().strip_prefix(TESTDATA_DIR).unwrap(); + if relative.parent().is_none() { + continue; + } + + #[cfg(unix)] + let decoded_bytes: Cow<[u8]> = percent_decode(relative.as_os_str().as_bytes()).into(); + #[cfg(unix)] + let decoded = OsStr::from_bytes(&decoded_bytes); + + #[cfg(windows)] + let decoded_bytes = relative + .as_os_str() + .encode_wide() + .map(|b| b as u8) + .collect::>(); + #[cfg(windows)] + let decoded_bytes = percent_decode(decoded_bytes.as_slice()) + .map(|b| (if b == b':' { b';' } else { b }) as u16) + .collect::>(); + #[cfg(windows)] + let decoded = OsString::from_wide(decoded_bytes.as_slice()); + + if entry.path().is_dir() { + fs::create_dir(tmp_path.join(&decoded)).expect("could not create directory"); + } else { + fs::copy(entry.path(), tmp_path.join(decoded)).expect("could not copy test data"); + } + } + func(Maildir::from(tmp_path.join(name))); +} + +fn with_maildir_empty(name: &str, func: F) +where + F: FnOnce(Maildir), +{ + let tmp_dir = tempdir().expect("could not create temporary directory"); + let tmp_path = tmp_dir.path(); + func(Maildir::from(tmp_path.join(name))); +} + +#[test] +fn maildir_count() { + with_maildir(MAILDIR_NAME, |maildir| { + assert_eq!(maildir.count_cur(), 1); + assert_eq!(maildir.count_new(), 1); + }); +} + +#[test] +fn maildir_list() { + with_maildir(MAILDIR_NAME, |maildir| { + let mut iter = maildir.list_new(); + let mut first = iter.next().unwrap().unwrap(); + assert_eq!(first.id(), "1463941010.5f7fa6dd4922c183dc457d033deee9d7"); + assert_eq!( + first.headers().unwrap().get_first_value("Subject"), + Some(String::from("test")) + ); + assert_eq!(first.is_seen(), false); + let second = iter.next(); + assert!(second.is_none()); + + let mut iter = maildir.list_cur(); + let mut first = iter.next().unwrap().unwrap(); + assert_eq!(first.id(), "1463868505.38518452d49213cb409aa1db32f53184"); + assert_eq!( + first.parsed().unwrap().headers.get_first_value("Subject"), + Some(String::from("test")) + ); + assert_eq!(first.is_seen(), true); + let second = iter.next(); + assert!(second.is_none()); + }) +} + +#[test] +fn maildir_list_subdirs() { + with_maildir(SUBMAILDIRS_NAME, |maildir| { + let subdirs: Vec<_> = maildir + .list_subdirs() + .map(|dir| { + dir.unwrap() + .path() + .file_name() + .unwrap() + .to_string_lossy() + .to_string() + }) + .collect(); + + assert_eq!(2, subdirs.len()); + assert!(subdirs.contains(&".Subdir1".into())); + assert!(subdirs.contains(&".Subdir2".into())); + assert!(!subdirs.contains(&"..Subdir3".into())); + }); +} + +#[test] +fn maildir_find() { + with_maildir(MAILDIR_NAME, |maildir| { + assert_eq!( + maildir + .find("1463941010.5f7fa6dd4922c183dc457d033deee9d7") + .is_some(), + true + ); + assert_eq!( + maildir + .find("1463868505.38518452d49213cb409aa1db32f53184") + .is_some(), + true + ); + }) +} + +#[test] +fn check_delete() { + with_maildir(MAILDIR_NAME, |maildir| { + assert_eq!( + maildir + .find("1463941010.5f7fa6dd4922c183dc457d033deee9d7") + .is_some(), + true + ); + assert_eq!( + maildir + .delete("1463941010.5f7fa6dd4922c183dc457d033deee9d7") + .is_ok(), + true + ); + assert_eq!( + maildir + .find("1463941010.5f7fa6dd4922c183dc457d033deee9d7") + .is_some(), + false + ); + }) +} + +#[test] +fn check_copy_and_move() { + with_maildir(MAILDIR_NAME, |maildir| { + with_maildir(SUBMAILDIRS_NAME, |submaildir| { + let id = "1463868505.38518452d49213cb409aa1db32f53184"; + + // check that we cannot copy a message from and to the same maildir + assert_eq!( + maildir.copy_to(id, &maildir).unwrap_err().kind(), + std::io::ErrorKind::InvalidInput, + ); + + // check that the message is present in "maildir" but not in "submaildir" + assert!(maildir.find(id).is_some()); + assert!(submaildir.find(id).is_none()); + // also check that the failed self-copy a few lines up didn't corrupt the + // message file. + assert!(maildir.find(id).unwrap().date().is_ok()); + + // copy the message from "maildir" to "submaildir" + maildir.copy_to(id, &submaildir).unwrap(); + + // check that the message is now present in both + assert!(maildir.find(id).is_some()); + assert!(submaildir.find(id).is_some()); + + // move the message from "submaildir" to "maildir" + submaildir.move_to(id, &maildir).unwrap(); + + // check that the message is now only present in "maildir" + assert!(maildir.find(id).is_some()); + assert!(submaildir.find(id).is_none()); + }) + }) +} + +#[test] +fn mark_read() { + with_maildir(MAILDIR_NAME, |maildir| { + assert_eq!( + maildir + .move_new_to_cur("1463941010.5f7fa6dd4922c183dc457d033deee9d7") + .unwrap(), + () + ); + }); +} + +#[test] +fn check_received() { + with_maildir(MAILDIR_NAME, |maildir| { + let mut iter = maildir.list_cur(); + let mut first = iter.next().unwrap().unwrap(); + assert_eq!(first.received().unwrap(), 1_463_868_507); + }); +} + +#[test] +fn check_create_dirs() { + with_maildir_empty("maildir2", |maildir| { + assert!(!maildir.path().exists()); + for name in &["cur", "new", "tmp"] { + assert!(!maildir.path().join(name).exists()); + } + + maildir.create_dirs().unwrap(); + assert!(maildir.path().exists()); + for name in &["cur", "new", "tmp"] { + assert!(maildir.path().join(name).exists()); + } + }); +} + +const TEST_MAIL_BODY: &[u8] = b"Return-Path: +X-Original-To: of82ecuq@cip.cs.fau.de +Delivered-To: of82ecuq@cip.cs.fau.de +Received: from faui0fl.informatik.uni-erlangen.de (unknown [IPv6:2001:638:a000:4160:131:188:60:117]) + by faui03.informatik.uni-erlangen.de (Postfix) with ESMTP id 466C1240A3D + for ; Fri, 12 May 2017 10:09:45 +0000 (UTC) +Received: by faui0fl.informatik.uni-erlangen.de (Postfix, from userid 303135) + id 389CC10E1A32; Fri, 12 May 2017 12:09:45 +0200 (CEST) +To: of82ecuq@cip.cs.fau.de +MIME-Version: 1.0 +Content-Type: text/plain; charset=\"UTF-8\" +Content-Transfer-Encoding: 8bit +Message-Id: <20170512100945.389CC10E1A32@faui0fl.informatik.uni-erlangen.de> +Date: Fri, 12 May 2017 12:09:45 +0200 (CEST) +From: of82ecuq@cip.cs.fau.de (Johannes Schilling) +Subject: maildir delivery test mail + +Today is Boomtime, the 59th day of Discord in the YOLD 3183"; + +#[test] +fn check_store_new() { + with_maildir_empty("maildir2", |maildir| { + maildir.create_dirs().unwrap(); + + assert_eq!(maildir.count_new(), 0); + let id = maildir.store_new(TEST_MAIL_BODY); + assert!(id.is_ok()); + 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| { + maildir.create_dirs().unwrap(); + let testflags = "FRS"; + + assert_eq!(maildir.count_cur(), 0); + maildir + .store_cur_with_flags(TEST_MAIL_BODY, testflags) + .unwrap(); + assert_eq!(maildir.count_cur(), 1); + + let mut iter = maildir.list_cur(); + let first = iter.next().unwrap().unwrap(); + assert_eq!(first.flags(), testflags); + }); +} + +#[test] +fn check_flag_fiddling() { + with_maildir_empty("maildir2", |maildir| { + maildir.create_dirs().unwrap(); + let id = maildir.store_cur_with_flags(TEST_MAIL_BODY, "SR").unwrap(); + + assert_eq!(maildir.count_cur(), 1); + assert_eq!(maildir.find(&id).unwrap().flags(), "RS"); + maildir.remove_flags(&id, "FS").unwrap(); + assert_eq!(maildir.find(&id).unwrap().flags(), "R"); + maildir.add_flags(&id, "RF").unwrap(); + assert_eq!(maildir.find(&id).unwrap().flags(), "FR"); + maildir.set_flags(&id, "SF").unwrap(); + assert_eq!(maildir.find(&id).unwrap().flags(), "FS"); + }); +}